# -*- coding: utf-8 -*-

# Copyright 2019-2026 Mike Fährmann
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.

"""Extractors for https://www.weibo.com/"""

from .common import Extractor, Message, Dispatch
from .. import text, util
import random

BASE_PATTERN = r"(?:https?://)?(?:www\.|m\.)?weibo\.c(?:om|n)"
USER_PATTERN = BASE_PATTERN + r"/(?:(u|n|p(?:rofile)?)/)?([^/?#]+)(?:/home)?"


class WeiboExtractor(Extractor):
    category = "weibo"
    directory_fmt = ("{category}", "{user[screen_name]}")
    filename_fmt = "{status[id]}_{num:>02}.{extension}"
    archive_fmt = "{status[id]}_{num}"
    cookies_domain = ".weibo.com"
    cookies_names = ("SUB", "SUBP")
    root = "https://weibo.com"
    request_interval = (1.0, 2.0)

    def __init__(self, match):
        Extractor.__init__(self, match)
        self._prefix = match[1]
        self.user = match[2]

    def _init(self):
        self.livephoto = self.config("livephoto", True)
        self.retweets = self.config("retweets", False)
        self.longtext = self.config("text", False)
        self.videos = self.config("videos", True)
        self.movies = self.config("movies", False)
        self.likes = self.config("likes", False)
        self.gifs = self.config("gifs", True)
        self.gifs_video = (self.gifs == "video")

        cookies = self.cache(
            _cookie_cache, _key=None, _exp=365*86400, _mem=False)
        if cookies is None:
            self.logged_in = self.cookies_check(
                self.cookies_names, self.cookies_domain)
            return

        domain = self.cookies_domain
        cookies = {c.name: c for c in cookies if c.domain == domain}
        for cookie in self.cookies:
            if cookie.domain == domain and cookie.name in cookies:
                del cookies[cookie.name]
                if not cookies:
                    self.logged_in = True
                    return

        self.logged_in = False
        for cookie in cookies.values():
            self.cookies.set_cookie(cookie)

    def request(self, url, **kwargs):
        response = Extractor.request(self, url, **kwargs)

        if response.history:
            if "login.sina.com" in response.url:
                raise self.exc.AbortExtraction(
                    f"HTTP redirect to login page "
                    f"({response.url.partition('?')[0]})")
            if "passport.weibo.com" in response.url:
                self._sina_visitor_system(response)
                response = Extractor.request(self, url, **kwargs)

        return response

    def items(self):
        original_retweets = (self.retweets == "original")
        is_like = text.re(r"\d-\d\d?赞过").search

        for status in self.statuses():

            if "ori_mid" in status and not self.retweets:
                self.log.debug("Skipping %s (快转 retweet)", status["id"])
                continue

            if "retweeted_status" in status:
                if not self.retweets:
                    self.log.debug("Skipping %s (retweet)", status["id"])
                    continue

                # videos of the original post are in status
                # images of the original post are in status["retweeted_status"]
                files = []
                self._extract_retweet(status, files)
                self._extract_status(status["retweeted_status"], files)

                if original_retweets:
                    status = status["retweeted_status"]
            else:
                files = []
                self._extract_status(status, files)

            if title := status.get("title"):
                if is_like(title.get("text") or ""):
                    if not self.likes:
                        self.log.debug("Skipping %s (赞过 like)", status["id"])
                        continue
                    status["like"] = True
                else:
                    status["like"] = False
            else:
                status["like"] = None

            if self.longtext and status.get("isLongText") and \
                    status["text"].endswith('class="expand">展开</span>'):
                status = self._status_by_id(status["id"])

            status["date"] = self.parse_datetime(
                status["created_at"], "%a %b %d %H:%M:%S %z %Y")
            status["count"] = len(files)
            yield Message.Directory, "", status

            num = 0
            for file in files:
                url = file["url"]
                if not url:
                    continue
                if url.startswith("http:"):
                    url = "https:" + url[5:]
                if "filename" not in file:
                    text.nameext_from_url(url, file)
                    if file["extension"] == "json":
                        file["extension"] = "mp4"
                if file["extension"] == "m3u8":
                    url = "ytdl:" + url
                    file["_ytdl_manifest"] = "hls"
                    file["extension"] = "mp4"
                num += 1
                file["status"] = status
                file["num"] = num
                yield Message.Url, url, file

    def _extract_status(self, status, files):
        if "mix_media_info" in status:
            for item in status["mix_media_info"]["items"]:
                type = item.get("type")
                if type == "video":
                    if self.videos:
                        files.append(self._extract_video(
                            item["data"]["media_info"]))
                elif type == "pic":
                    files.append(item["data"]["largest"].copy())
                else:
                    self.log.warning("Unknown media type '%s'", type)
            return

        if pic_ids := status.get("pic_ids"):
            pics = status["pic_infos"]
            for pic_id in pic_ids:
                pic = pics[pic_id]
                pic_type = pic.get("type")

                if pic_type == "gif" and self.gifs:
                    if self.gifs_video:
                        files.append({"url": pic["video"]})
                    else:
                        files.append(pic["largest"].copy())

                elif pic_type == "livephoto" and self.livephoto:
                    files.append(pic["largest"].copy())
                    files.append({"url": pic["video"]})

                else:
                    files.append(pic["largest"].copy())

        if "page_info" in status:
            info = status["page_info"]
            if "media_info" in info and self.videos:
                if info.get("type") != "5" or self.movies:
                    files.append(self._extract_video(info["media_info"]))
                else:
                    self.log.debug("%s: Ignoring 'movie' video", status["id"])

    def _extract_retweet(self, status, files):
        self._extract_status(status, files)
        if "url_struct" in status:
            for item in status["url_struct"]:
                if pics := item.get("pic_infos"):
                    for pic in pics.values():
                        files.append(pic.get("largest") or pic["large"])

    def _extract_video(self, info):
        if info.get("live_status") == 1:
            self.log.debug("Skipping ongoing live stream")
            return {"url": ""}

        try:
            media = max(info["playback_list"],
                        key=lambda m: m["meta"]["quality_index"])
        except Exception:
            video = {"url": (info.get("replay_hd") or
                             info.get("stream_url_hd") or
                             info.get("stream_url") or "")}
        else:
            video = media["play_info"].copy()

        if "//wblive-out." in video["url"] and \
                not text.ext_from_url(video["url"]):
            try:
                video["url"] = self.request_location(video["url"])
            except self.exc.HttpError as exc:
                self.log.warning("%s: %s", exc.__class__.__name__, exc)
                video["url"] = ""

        return video

    def _status_by_id(self, status_id):
        url = (f"{self.root}/ajax/statuses/show"
               f"?id={status_id}&isGetLongText=true")
        return self.request_json(url)

    def _user(self, user):
        url = (f"{self.root}/ajax/profile/info?"
               f"{'screen_name' if self._prefix == 'n' else 'custom'}={user}")
        return self.request_json(url, interval=False)["data"]["user"]

    def _user_id(self):
        user = self.user
        if len(user) >= 10 and user.isdecimal():
            return user[-10:]
        else:
            return self._user(user)["idstr"]

    def _pagination(self, endpoint, params,
                    since_key="sinceid", subalbums=None):
        url = f"{self.root}/ajax{endpoint}"
        headers = {
            "Accept": "application/json, text/plain, */*",
            "X-Requested-With": "XMLHttpRequest",
            "X-XSRF-TOKEN": None,
            "Referer": f"{self.root}/u/{params['uid']}",
        }

        while True:
            response = self.request(url, params=params, headers=headers)
            headers["X-XSRF-TOKEN"] = response.cookies.get("XSRF-TOKEN")

            data = response.json()
            if not (ok := data.get("ok", 0)):
                self.log.debug(response.content)
                if "since_id" not in params:  # first iteration
                    raise self.exc.AbortExtraction(
                        f'"{data.get("msg") or "unknown error"}"')
            elif ok < 0:
                self.request(self.root + "/").content
                continue

            try:
                data = data["data"]
                statuses = data["list"]
            except KeyError:
                return

            if subalbums is not None:
                subalbums = None
                yield data.get("album_list") or ()

            yield from statuses

            # videos, newvideo
            if cursor := data.get("next_cursor"):
                if cursor == -1:
                    return
                params["cursor"] = cursor
                continue

            # album
            if "since_id" in data:
                params[since_key] = since_id = data["since_id"]
                if not since_id:
                    return
                if "page" in params:
                    params["page"] += 1
                continue

            # home, article
            if "page" in params:
                if not statuses:
                    return
                params["page"] += 1
                continue

            # feed, last album page
            try:
                params["since_id"] = statuses[-1]["id"] - 1
            except LookupError:
                return

    def _sina_visitor_system(self, response):
        self.log.info("Sina Visitor System")

        passport_url = "https://passport.weibo.com/visitor/genvisitor2"
        headers = {"Referer": response.url}
        data = {
            "cb"  : "visitor_gray_callback",
            "ver" : "20250916",
            "tid" : "",
            "from": "weibo",
            "webdriver" : "false",
            "return_url": "https://weibo.com/",
        }

        page = Extractor.request(
            self, passport_url, method="POST", headers=headers, data=data).text
        data = util.json_loads(text.extr(page, "(", ");"))["data"]

        passport_url = "https://passport.weibo.com/visitor/visitor"
        params = {
            "a"    : "crossdomain",
            "t"    : data.get("tid"),
            "sp"   : data["subp"],
            "s"    : data["sub"],
            "from" : "weibo",
            "_rand": random.random(),
            "url"  : response.url
        }
        response = Extractor.request(
            self, passport_url, params=params, allow_redirects=False)
        self.cache_update(
            _cookie_cache, ..., response.cookies, _exp=365*86400)


class WeiboUserExtractor(WeiboExtractor):
    """Extractor for weibo user profiles"""
    subcategory = "user"
    pattern = USER_PATTERN + r"(?:$|#)"
    example = "https://weibo.com/USER"

    # do NOT override 'initialize()'
    # it is needed for 'self._user_id()'
    # def initialize(self):
    #     pass

    def items(self):
        base = f"{self.root}/u/{self._user_id()}?tabtype="
        return Dispatch._dispatch_extractors(self, (
            (WeiboHomeExtractor    , base + "home"),
            (WeiboFeedExtractor    , base + "feed"),
            (WeiboVideosExtractor  , base + "video"),
            (WeiboNewvideoExtractor, base + "newVideo"),
            (WeiboArticleExtractor , base + "article"),
            (WeiboAlbumExtractor   , base + "album"),
        ), ("feed",), {
            ("album", "subalbums", base + "album-only"),
        })


class WeiboHomeExtractor(WeiboExtractor):
    """Extractor for weibo 'home' listings"""
    subcategory = "home"
    pattern = USER_PATTERN + r"\?tabtype=home"
    example = "https://weibo.com/USER?tabtype=home"

    def statuses(self):
        endpoint = "/profile/myhot"
        params = {"uid": self._user_id(), "page": 1, "feature": "2"}
        return self._pagination(endpoint, params)


class WeiboFeedExtractor(WeiboExtractor):
    """Extractor for weibo user feeds"""
    subcategory = "feed"
    pattern = USER_PATTERN + r"\?tabtype=feed"
    example = "https://weibo.com/USER?tabtype=feed"

    def statuses(self):
        endpoint = "/statuses/mymblog"
        params = {"uid": self._user_id(), "feature": "0"}
        if self.logged_in:
            params["page"] = 1
        return self._pagination(endpoint, params)


class WeiboVideosExtractor(WeiboExtractor):
    """Extractor for weibo 'video' listings"""
    subcategory = "videos"
    pattern = USER_PATTERN + r"\?tabtype=video"
    example = "https://weibo.com/USER?tabtype=video"

    def statuses(self):
        endpoint = "/profile/getprofilevideolist"
        params = {"uid": self._user_id()}

        for status in self._pagination(endpoint, params):
            yield status["video_detail_vo"]


class WeiboNewvideoExtractor(WeiboExtractor):
    """Extractor for weibo 'newVideo' listings"""
    subcategory = "newvideo"
    pattern = USER_PATTERN + r"\?tabtype=newVideo"
    example = "https://weibo.com/USER?tabtype=newVideo"

    def statuses(self):
        endpoint = "/profile/getWaterFallContent"
        params = {"uid": self._user_id()}
        return self._pagination(endpoint, params)


class WeiboArticleExtractor(WeiboExtractor):
    """Extractor for weibo 'article' listings"""
    subcategory = "article"
    pattern = USER_PATTERN + r"\?tabtype=article"
    example = "https://weibo.com/USER?tabtype=article"

    def statuses(self):
        endpoint = "/statuses/mymblog"
        params = {"uid": self._user_id(), "page": 1, "feature": "10"}
        return self._pagination(endpoint, params)


class WeiboAlbumExtractor(WeiboExtractor):
    """Extractor for weibo 'album' listings"""
    subcategory = "album"
    pattern = USER_PATTERN + r"\?tabtype=album(?:[:_-]([^&#]+))?"
    example = "https://weibo.com/USER?tabtype=album"

    def items(self):
        subalbum = self.groups[2]

        if not subalbum and not self.config("subalbums", False):
            return WeiboExtractor.items(self)

        self.directory_fmt = ("{category}", "{user[screen_name]}",
                              "Album", "{subalbum[pic_title]|''}")
        self.filename_fmt = "{filename}.{extension}"
        self.archive_fmt = "{subalbum[pic_title]}_{pid}"
        return self.items_subalbum(subalbum)

    def items_subalbum(self, subalbum):
        user = self.kwdict["user"] = self._user(self.user)
        base = self.root + "/ajax/common/download?pid="

        for data, files in self.albums(user["idstr"], subalbum):
            self.kwdict["subalbum"] = data
            yield Message.Directory, "", {}
            for file in files:
                if "pid" in file:
                    file["filename"] = file["pid"]
                    file["extension"] = "jpg"
                    yield Message.Url, base + file["pid"], file
                elif "mid" in file:
                    mid = file["mid"]
                    status = self._status_by_id(mid)
                    if status.get("ok") != 1:
                        self.log.debug("Skipping status %s (%s)", mid, status)
                    else:
                        self.statuses = lambda: (status,)
                        yield from WeiboExtractor.items(self)
                        yield Message.Directory, "", {}

    def statuses(self):
        endpoint = "/profile/getImageWall"
        params = {"uid": self._user_id()}

        seen = set()
        for image in self._pagination(endpoint, params):
            mid = image["mid"]
            if mid not in seen:
                seen.add(mid)
                status = self._status_by_id(mid)
                if status.get("ok") != 1:
                    self.log.debug("Skipping status %s (%s)", mid, status)
                else:
                    yield status

    def albums(self, uid, subalbum):
        endpoint = "/profile/getImageWall"
        params = {
            "uid"      : uid,
            "sinceid"  : "0",
            "has_album": "true",
        }
        album = self._pagination(endpoint, params, subalbums=True)
        subalbums = next(album, ())

        if not subalbum or subalbum == "0":
            return (({}, album),)

        if subalbum == "all":
            results = [
                (sub, self._pagination_subalbum(uid, sub))
                for sub in subalbums
            ]
            results.append(({}, album))
            return results

        if subalbum == "only":
            return [
                (sub, self._pagination_subalbum(uid, sub))
                for sub in subalbums
            ]

        if subalbum.isdecimal():
            try:
                sub = subalbums[int(subalbum)-1]
            except Exception:
                raise self.exc.NotFoundError("subalbum")
        else:
            subalbum = text.unquote(subalbum)
            for sub in subalbums:
                if sub["pic_title"] == subalbum:
                    break
            else:
                raise self.exc.NotFoundError("subalbum")
        return ((sub, self._pagination_subalbum(uid, sub)),)

    def _pagination_subalbum(self, uid, sub):
        params = {"uid": uid, "containerid": text.unquote(sub["containerid"])}
        return self._pagination("/profile/getAlbumDetail", params, "since_id")


class WeiboStatusExtractor(WeiboExtractor):
    """Extractor for a weibo status"""
    subcategory = "status"
    pattern = BASE_PATTERN + r"/(detail|status|\d+)/(\w+)"
    example = "https://weibo.com/detail/12345"

    def statuses(self):
        status = self._status_by_id(self.user)
        if status.get("ok") != 1:
            self.log.debug(status)
            raise self.exc.NotFoundError("status")
        return (status,)


def _cookie_cache():
    return None
