# coding=utf-8

import os
import re
import shutil
from datetime import datetime

import rsa
import math
import base64
import hashlib
import requests
from urllib import parse


class VideoPart:
    def __init__(self, path, title='', desc=''):
        self.path = path
        self.title = title
        self.desc = desc


class Bilibili:
    def __init__(self, cookie=None):
        self.files = []
        self.videos = []
        self.session = requests.session()
        if cookie:
            self.session.headers["cookie"] = cookie
            self.csrf = re.search('bili_jct=(.*?);', cookie).group(1)
            self.mid = re.search('DedeUserID=(.*?);', cookie).group(1)
            self.session.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
            self.session.headers['Referer'] = 'https://space.bilibili.com/{mid}/#!/'.format(mid=self.mid)
            # session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36'
            # session.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'

    def login(self, user, pwd):
        """

        :param user: username
        :type user: str
        :param pwd: password
        :type pwd: str
        :return: if success return True
                 else return msg json
        """
        APPKEY = '1d8b6e7d45233436'
        ACTIONKEY = 'appkey'
        BUILD = 520001
        DEVICE = 'android'
        MOBI_APP = 'android'
        PLATFORM = 'android'
        APPSECRET = '560c52ccd288fed045859ed18bffd973'

        def md5(s):
            h = hashlib.md5()
            h.update(s.encode('utf-8'))
            return h.hexdigest()

        def sign(s):
            """

            :return: return sign
            """
            return md5(s + APPSECRET)

        def signed_body(body):
            """

            :return: body which be added sign
            """
            if isinstance(body, str):
                return body + '&sign=' + sign(body)
            elif isinstance(body, dict):
                ls = []
                for k, v in body.items():
                    ls.append(k + '=' + v)
                body['sign'] = sign('&'.join(ls))
                return body

        def getkey():
            """

            :return: hash, key
            """
            r = self.session.post(
                'https://passport.bilibili.com/api/oauth2/getKey',
                signed_body({'appkey': APPKEY}),
            )
            # {"ts":1544152439,"code":0,"data":{"hash":"99c7573759582e0b","key":"-----BEGIN PUBLIC----- -----END PUBLIC KEY-----\n"}}
            json = r.json()
            data = json['data']
            return data['hash'], data['key']

        def cnn_captcha(img):
            url = "http://47.95.255.188:5000/code"
            data = {"image": img}
            r = requests.post(url, data=data)
            return r.text

        self.session.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
        h, k = getkey()
        pwd = base64.b64encode(
            rsa.encrypt(
                (h + pwd).encode('utf-8'),
                rsa.PublicKey.load_pkcs1_openssl_pem(k.encode())
            )
        )
        user = parse.quote_plus(user)
        pwd = parse.quote_plus(pwd)

        r = self.session.post(
            'https://passport.bilibili.com/api/v2/oauth2/login',
            signed_body('appkey={appkey}&password={password}&username={username}'
                        .format(appkey=APPKEY, username=user, password=pwd))
        )
        try:
            json = r.json()
        except:
            return r.text

        if json['code'] == -105:
            # need captcha
            self.session.headers['cookie'] = 'sid=xxxxxxxx'
            r = self.session.get('https://passport.bilibili.com/captcha')
            captcha = cnn_captcha(base64.b64encode(r.content))
            r = self.session.post(
                'https://passport.bilibili.com/api/v2/oauth2/login',
                signed_body('actionKey={actionKey}&appkey={appkey}&build={build}&captcha={captcha}&device={device}'
                            '&mobi_app={mobi_app}&password={password}&platform={platform}&username={username}'
                            .format(actionKey=ACTIONKEY,
                                    appkey=APPKEY,
                                    build=BUILD,
                                    captcha=captcha,
                                    device=DEVICE,
                                    mobi_app=MOBI_APP,
                                    password=pwd,
                                    platform=PLATFORM,
                                    username=user)),
            )
            json = r.json()

        if json['code'] is not 0:
            return r.text

        ls = []
        for item in json['data']['cookie_info']['cookies']:
            ls.append(item['name'] + '=' + item['value'])
        cookie = '; '.join(ls)
        self.session.headers["cookie"] = cookie

        self.csrf = re.search('bili_jct=(.*?);', cookie).group(1)
        self.mid = re.search('DedeUserID=(.*?);', cookie).group(1)
        self.session.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
        self.session.headers['Referer'] = 'https://space.bilibili.com/{mid}/#!/'.format(mid=self.mid)

        return True

    def upload(self,
               parts,
               title,
               tid,
               tag,
               desc,
               source='',
               cover='',
               no_reprint=1,
               ):
        """

        :param parts: e.g. VideoPart('part path', 'part title', 'part desc'), or [VideoPart(...), VideoPart(...)]
        :type parts: VideoPart or list<VideoPart>
        :param title: video's title
        :type title: str
        :param tid: video type, see: https://member.bilibili.com/x/web/archive/pre
                                  or https://github.com/uupers/BiliSpider/wiki/%E8%A7%86%E9%A2%91%E5%88%86%E5%8C%BA%E5%AF%B9%E5%BA%94%E8%A1%A8
        :type tid: int
        :param tag: video's tag
        :type tag: list<str>
        :param desc: video's description
        :type desc: str
        :param source: (optional) 转载地址
        :type source: str
        :param cover: (optional) cover's URL, use method *cover_up* to get
        :type cover: str
        :param no_reprint: (optional) 0=可以转载, 1=禁止转载(default)
        :type no_reprint: int
        """
        self.preUpload(parts)
        self.finishUpload(title, tid, tag, desc, source, cover, no_reprint)

    def preUpload(self, parts):
        """
        :param parts: e.g. VideoPart('part path', 'part title', 'part desc'), or [VideoPart(...), VideoPart(...)]
        :type parts: VideoPart or list<VideoPart>
        """

        self.session.headers['Content-Type'] = 'application/json; charset=utf-8'
        if not isinstance(parts, list):
            parts = [parts]

        for part in parts:
            filepath = part.path
            filename = os.path.basename(filepath)
            filesize = os.path.getsize(filepath)
            self.files.append(part)
            r = self.session.get('https://member.bilibili.com/preupload?'
                                 'os=upos&upcdn=ws&name={name}&size={size}&r=upos&profile=ugcupos%2Fyb&ssl=0'
                                 .format(name=filename, size=filesize))
            """return example
            {
                "upos_uri": "upos://ugc/i181012ws18x52mti3gg0h33chn3tyhp.mp4",
                "biz_id": 58993125,
                "endpoint": "//upos-hz-upcdnws.acgvideo.com",
                "endpoints": [
                    "//upos-hz-upcdnws.acgvideo.com",
                    "//upos-hz-upcdntx.acgvideo.com"
                ],
                "chunk_retry_delay": 3,
                "chunk_retry": 200,
                "chunk_size": 4194304,
                "threads": 2,
                "timeout": 900,
                "auth": "os=upos&cdn=upcdnws&uid=&net_state=4&device=&build=&os_version=&ak=×tamp=&sign=",
                "OK": 1
            }
            """
            json = r.json()
            upos_uri = json['upos_uri']
            endpoint = json['endpoint']
            auth = json['auth']
            biz_id = json['biz_id']
            chunk_size = json['chunk_size']
            self.session.headers['X-Upos-Auth'] = auth  # add auth header
            r = self.session.post(
                'https:{}/{}?uploads&output=json'.format(endpoint, upos_uri.replace('upos://', '')))
            # {"upload_id":"72eb747b9650b8c7995fdb0efbdc2bb6","key":"\/i181012ws2wg1tb7tjzswk2voxrwlk1u.mp4","OK":1,"bucket":"ugc"}
            json = r.json()
            upload_id = json['upload_id']

            with open(filepath, 'rb') as f:
                chunks_num = math.ceil(filesize / chunk_size)
                chunks_index = 0
                chunks_data = f.read(chunk_size)
                while True:
                    if not chunks_data:
                        break
                    r = self.session.put('https:{endpoint}/{upos_uri}?'
                                         'partNumber={part_number}&uploadId={upload_id}&chunk={chunk}&chunks={chunks}&size={size}&start={start}&end={end}&total={total}'
                                         .format(endpoint=endpoint,
                                                 upos_uri=upos_uri.replace('upos://', ''),
                                                 part_number=chunks_index + 1,  # starts with 1
                                                 upload_id=upload_id,
                                                 chunk=chunks_index,
                                                 chunks=chunks_num,
                                                 size=len(chunks_data),
                                                 start=chunks_index * chunk_size,
                                                 end=chunks_index * chunk_size + len(chunks_data),
                                                 total=filesize,
                                                 ),
                                         chunks_data,
                                         )
                    if r.status_code != 200:
                        continue
                    print('{} : UPLOAD {}/{}'.format(datetime.strftime(datetime.now(), "%y%m%d %H%M"), chunks_index,
                                                     chunks_num), r.text)
                    chunks_data = f.read(chunk_size)
                    chunks_index += 1  # start with 0

                # NOT DELETE! Refer to https://github.com/comwrg/bilibiliupload/issues/15#issuecomment-424379769
                self.session.post('https:{endpoint}/{upos_uri}?'
                                  'output=json&name={name}&profile=ugcupos%2Fyb&uploadId={upload_id}&biz_id={biz_id}'
                                  .format(endpoint=endpoint,
                                          upos_uri=upos_uri.replace('upos://', ''),
                                          name=filename,
                                          upload_id=upload_id,
                                          biz_id=biz_id,
                                          ),
                                  {"parts": [{"partNumber": i, "eTag": "etag"} for i in range(1, chunks_num + 1)]},
                                  )

            self.videos.append({'filename': upos_uri.replace('upos://ugc/', '').split('.')[0],
                                'title': part.title,
                                'desc': part.desc})

    def finishUpload(self,
                     title,
                     tid,
                     tag,
                     desc,
                     source='',
                     cover='',
                     no_reprint=1,
                     ):
        """
        :param title: video's title
        :type title: str
        :param tid: video type, see: https://member.bilibili.com/x/web/archive/pre
                                  or https://github.com/uupers/BiliSpider/wiki/%E8%A7%86%E9%A2%91%E5%88%86%E5%8C%BA%E5%AF%B9%E5%BA%94%E8%A1%A8
        :type tid: int
        :param tag: video's tag
        :type tag: list<str>
        :param desc: video's description
        :type desc: str
        :param source: (optional) 转载地址
        :type source: str
        :param cover: (optional) cover's URL, use method *cover_up* to get
        :type cover: str
        :param no_reprint: (optional) 0=可以转载, 1=禁止转载(default)
        :type no_reprint: int
        """
        if len(self.videos) == 0:
            return
        self.session.headers['Content-Type'] = 'application/json; charset=utf-8'
        copyright = 2 if source else 1
        r = self.session.post('https://member.bilibili.com/x/vu/web/add?csrf=' + self.csrf,
                              json={
                                  "copyright": copyright,
                                  "source": source,
                                  "title": title,
                                  "tid": tid,
                                  "tag": ','.join(tag),
                                  "no_reprint": no_reprint,
                                  "desc": desc,
                                  "cover": cover,
                                  "mission_id": 0,
                                  "order_id": 0,
                                  "videos": self.videos}
                              )
        print(r.text)

    def clear(self):
        self.files.clear()
        self.videos.clear()

    def appendUpload(self,
                     aid,
                     parts,
                     title="",
                     tid="",
                     tag="",
                     desc="",
                     source='',
                     cover='',
                     no_reprint=1,
                     ):
        """
        :param aid: just aid
        :type aid: int
        :param parts: e.g. VideoPart('part path', 'part title', 'part desc'), or [VideoPart(...), VideoPart(...)]
        :type parts: VideoPart or list<VideoPart>
        :param title: video's title
        :type title: str
        :param tid: video type, see: https://member.bilibili.com/x/web/archive/pre
                                  or https://github.com/uupers/BiliSpider/wiki/%E8%A7%86%E9%A2%91%E5%88%86%E5%8C%BA%E5%AF%B9%E5%BA%94%E8%A1%A8
        :type tid: int
        :param tag: video's tag
        :type tag: list<str>
        :param desc: video's description
        :type desc: str
        :param source: (optional) 转载地址
        :type source: str
        :param cover: (optional) cover's URL, use method *cover_up* to get
        :type cover: str
        :param no_reprint: (optional) 0=可以转载, 1=禁止转载(default)
        :type no_reprint: int
        """
        self.session.headers['Content-Type'] = 'application/json; charset=utf-8'
        p = self.session.get("https://member.bilibili.com/x/web/archive/view?aid={}&history=".format(aid))
        j = p.json()
        if len(self.videos) == 0:
            for i in j['data']['videos']:
                self.videos.append({'filename': i['filename'],
                                    'title': i["title"],
                                    'desc': i["desc"]})
        if (title == ""): title = j["data"]["archive"]['title']
        if (tag == ""): tag = j["data"]["archive"]['tag']
        if (no_reprint == ""): no_reprint = j["data"]["archive"]['no_reprint']
        if (desc == ""): desc = j["data"]["archive"]['desc']
        if (source == ""): source = j["data"]["archive"]['source']
        if (tid == ""): tid = j["data"]["archive"]['tid']
        self.preUpload(parts)
        self.editUpload(aid, title, tid, tag, desc, source, cover, no_reprint)

    def editUpload(self,
                   aid,
                   title,
                   tid,
                   tag,
                   desc,
                   source='',
                   cover='',
                   no_reprint=1,
                   ):
        """
        :param aid: just aid
        :type aid: int
        :param parts: e.g. VideoPart('part path', 'part title', 'part desc'), or [VideoPart(...), VideoPart(...)]
        :type parts: VideoPart or list<VideoPart>
        :param title: video's title
        :type title: str
        :param tid: video type, see: https://member.bilibili.com/x/web/archive/pre
                                  or https://github.com/uupers/BiliSpider/wiki/%E8%A7%86%E9%A2%91%E5%88%86%E5%8C%BA%E5%AF%B9%E5%BA%94%E8%A1%A8
        :type tid: int
        :param tag: video's tag
        :type tag: list<str>
        :param desc: video's description
        :type desc: str
        :param source: (optional) 转载地址
        :type source: str
        :param cover: (optional) cover's URL, use method *cover_up* to get
        :type cover: str
        :param no_reprint: (optional) 0=可以转载, 1=禁止转载(default)
        :type no_reprint: int
        """
        copyright = 2 if source else 1
        r = self.session.post('https://member.bilibili.com/x/vu/web/edit?csrf=' + self.csrf,
                              json={
                                  "aid": aid,
                                  "copyright": copyright,
                                  "source": source,
                                  "title": title,
                                  "tid": tid,
                                  "tag": ','.join(tag),
                                  "no_reprint": no_reprint,
                                  "desc": desc,
                                  "cover": cover,
                                  "mission_id": 0,
                                  "order_id": 0,
                                  "videos": self.videos}
                              )
        print(r.text)

    def addChannel(self, name, intro=''):
        """

        :param name: channel's name
        :type name: str
        :param intro: channel's introduction
        :type intro: str
        """
        r = self.session.post(
            url='https://space.bilibili.com/ajax/channel/addChannel',
            data={
                'name': name,
                'intro': intro,
                'aids': '',
                'csrf': self.csrf,
            },
            # name=123&intro=123&aids=&csrf=565d7ed17cef2cc8ad054210c4e64324&_=1497077610768

        )
        # return
        # {"status":true,"data":{"cid":"15812"}}
        print(r.json())

    def channel_addVideo(self, cid, aids):
        """

        :param cid: channel's id
        :type cid: int
        :param aids: videos' id
        :type aids: list<int>
        """

        r = self.session.post(
            url='https://space.bilibili.com/ajax/channel/addVideo',
            data={
                'aids': '%2C'.join(aids),
                'cid': cid,
                'csrf': self.csrf
            }
            # aids=9953555%2C9872953&cid=15814&csrf=565d7ed17cef2cc8ad054210c4e64324&_=1497079332679
        )
        print(r.json())

    def cover_up(self, img):
        """

        :param img: img path or stream
        :type img: str or BufferedReader
        :return: img URL
        """

        if isinstance(img, str):
            f = open(img, 'rb')
        else:
            f = img
        r = self.session.post(
            url='https://member.bilibili.com/x/vu/web/cover/up',
            data={
                'cover': b'data:image/jpeg;base64,' + (base64.b64encode(f.read())),
                'csrf': self.csrf,
            }
        )
        # print(r.text)
        # {"code":0,"data":{"url":"http://i0.hdslb.com/bfs/archive/67db4a6eae398c309244e74f6e85ae8d813bd7c9.jpg"},"message":"","ttl":1}
        return r.json()['data']['url']