"""
Moebooru Image Board API implementation.
"""

import asyncio
import os
from typing import AsyncIterable

import httpx
import pandas as pd
from asyncstdlib import enumerate as aenumerate
from httpx._types import AuthTypes
from lxml import etree

from .booru import Booru, BooruComponent
from .utils import logger

__all__ = [
    # base classes
    "Moebooru",
    "MoebooruComponent",
    # client classes
    "YandereClient",
    # component classes
    "YanderePosts",
    "YandereTags",
    "YandereArtists",
    "YandereComments",
    "YandereWiki",
    "YandereNotes",
    "YandereUsers",
    "YandereForum",
    "YanderePools",
    "YandereFavorites",
]


class Moebooru(Booru):
    """
    Moebooru Image Board API
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)


class MoebooruComponent(BooruComponent):
    """
    Moebooru Image Board Component
    """

    def __init__(self, client):
        super().__init__(client)


class YandereClient(Moebooru):
    """
    Yandere API reference document: https://yande.re/help/api
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        # 设置发送相对 URL 请求时使用的基础 URL
        self.base_url = "https://yande.re"

        # 初始化各组件
        self.posts = YanderePosts(self)
        # self.tags = YandereTags(self)
        # self.artists = YandereArtists(self)
        # self.comments = YandereComments(self)
        # self.wiki = YandereWiki(self)
        # self.notes = YandereNotes(self)
        # self.users = YandereUsers(self)
        # self.forum = YandereForum(self)
        self.pools = YanderePools(self)
        # self.favorites = YandereFavorites(self)

        # 登录后修改（默认无账号）

    def login(self, auth: AuthTypes):
        # TODO
        raise NotImplementedError("The method is not implemented")


class YanderePosts(MoebooruComponent):
    """
    Posts: https://yande.re/help/api#posts
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    async def list_gt_page(
        self,
        limit: int = 40,
        tags: str = "",
    ) -> int:
        """
        使用定位 html 分页器的方式，获取指定标签帖子列表的最大页码

        Note:
            对于 post 页面，返回的 html 分页器中的最大页码由于 yande.re 网站中的某些 Hidden Posts 策略（rating:e, blacklists .etc），实际的最大页码会大于等于该页码
            yande.re post 页面展示策略受 limit 参数影响，会自动根据 limit 参数与实际帖子数量调整 html 分页器中的最大页码

        Args:
            limit (int, optional): 您想检索多少篇帖子。每次请求的帖子数量有一个硬性限制，最多 1000 篇. Defaults to 40.
            tags (str, optional): 要搜索的标签。任何在网站上有效的标签组合在这里都有效。这包括所有元标签。要组合的不同标签使用空格连接，同一标签中的空格使用 _ 替换. Defaults to ''. 表示搜索全站

        Returns:
            int: html 分页器中的最大页码，实际的最大页码会大于等于该页码
        """
        url = "/post"
        params = {
            "limit": limit,  # 您想检索多少篇帖子。每次请求的帖子数量有一个硬性限制，最多 1000 篇
            "page": 1,  # 查询页码
            "tags": tags,  # 要搜索的标签。任何在网站上有效的标签组合在这里都有效。这包括所有元标签。要组合的不同标签使用空格连接，同一标签中的空格使用 _ 替换
        }

        try:
            response = await self.client.get(url, params=params)
            # 解析 html 分页器中的最大页码
            tree = etree.HTML(response.text)
            # 形如 ['2', '3', '4', '5', '1067', '1068', 'Next →'] 的样式。列表中的最后一个永远为 'Next →'；由于请求的 url 中的 page 参数固定为 1，当前页码信息 1 使用 em 标签而非 a 标签，故列表若存在，则永远以 2 开头
            pagination = tree.xpath('//div[@class="pagination"]/a[@aria-label]/text()')
            if pagination:  # 存在分页器，说明该页面至少有两页
                return int(pagination[-2])
            else:  # 不存在分页器，说明该页面只有一页
                return 1
        except httpx.HTTPError as exc:
            logger.error(f"{exc.__class__.__name__} for {exc.request.url} - {exc}")

    async def list(
        self,
        limit: int = 40,
        start_page: int = 1,
        end_page: int = 1,
        all_page: bool = False,
        tags: str = "",
    ) -> AsyncIterable[list[dict] | None]:
        """
        List

        The base URL is /post.xml.

        - limit: How many posts you want to retrieve. There is a hard limit of 100 posts per request.
        - page: The page number.
        - tags: The tags to search for. Any tag combination that works on the web site will work here. This includes all the meta-tags.

        Note:
            修订 API 参考文件: https://yande.re/help/api: limit: How many posts you want to retrieve. There is a hard limit of 100 posts per request. 将其更改为：limit: How many posts you want to retrieve. There is a hard limit of 1000 posts per request.

        Return json format:
        ```
        [
            {
                "id": 1223391,
                "tags": "akiyama_mio bleed_through chibi christmas dress fixme hirasawa_yui horiguchi_yukiko k-on! kotobuki_tsumugi nakano_azusa pantyhose tainaka_ritsu",
                "created_at": 1742150809,
                "updated_at": 1742156680,
                "creator_id": 537635,
                "approver_id": null,
                "author": "Reverseshin",
                "change": 6324472,
                "source": "Animedia 2010-12",
                "score": 3,
                "md5": "3a453ffae99c4de46e4fb5bf82236842",
                "file_size": 26463488,
                "file_ext": "png",
                "file_url": "https://files.yande.re/image/3a453ffae99c4de46e4fb5bf82236842/yande.re%201223391%20akiyama_mio%20bleed_through%20chibi%20christmas%20dress%20fixme%20hirasawa_yui%20horiguchi_yukiko%20k-on%21%20kotobuki_tsumugi%20nakano_azusa%20pantyhose%20tainaka_ritsu.png",
                "is_shown_in_index": false,
                "preview_url": "https://assets.yande.re/data/preview/3a/45/3a453ffae99c4de46e4fb5bf82236842.jpg",
                "preview_width": 150,
                "preview_height": 93,
                "actual_preview_width": 300,
                "actual_preview_height": 186,
                "sample_url": "https://files.yande.re/sample/3a453ffae99c4de46e4fb5bf82236842/yande.re%201223391%20sample%20akiyama_mio%20bleed_through%20chibi%20christmas%20dress%20fixme%20hirasawa_yui%20horiguchi_yukiko%20k-on%21%20kotobuki_tsumugi%20nakano_azusa%20pantyhose%20tainaka_ritsu.jpg",
                "sample_width": 1500,
                "sample_height": 931,
                "sample_file_size": 481240,
                "jpeg_url": "https://files.yande.re/jpeg/3a453ffae99c4de46e4fb5bf82236842/yande.re%201223391%20akiyama_mio%20bleed_through%20chibi%20christmas%20dress%20fixme%20hirasawa_yui%20horiguchi_yukiko%20k-on%21%20kotobuki_tsumugi%20nakano_azusa%20pantyhose%20tainaka_ritsu.jpg",
                "jpeg_width": 3500,
                "jpeg_height": 2172,
                "jpeg_file_size": 2088523,
                "rating": "s",
                "is_rating_locked": false,
                "has_children": false,
                "parent_id": 162305,
                "status": "active",
                "is_pending": false,
                "width": 5492,
                "height": 3408,
                "is_held": false,
                "frames_pending_string": "",
                "frames_pending": [],
                "frames_string": "",
                "frames": [],
                "is_note_locked": false,
                "last_noted_at": 0,
                "last_commented_at": 0
            },
            ...
        ]
        ```

        获取在起始页码与结束页码范围内，指定标签的帖子列表；若 all_page 为 True，则获取当前查询标签下所有页码的帖子列表

        Args:
            limit (int, optional): 您想检索多少篇帖子。每次请求的帖子数量有一个硬性限制，最多 1000 篇. Defaults to 40.
            start_page (int, optional): 查询起始页码. Defaults to 1.
            end_page (int, optional): 查询结束页码. Defaults to 1.
            all_page (bool, optional): 是否获取当前查询标签下所有页码的帖子列表，若为 True，则忽略 start_page 与 end_page 参数. Defaults to False.
            tags (str, optional): 要搜索的标签。任何在网站上有效的标签组合在这里都有效。这包括所有元标签。要组合的不同标签使用空格连接，同一标签中的空格使用 _ 替换. Defaults to ''. 表示搜索全站

        Yields:
            list[dict] | None. 若获取成功，则返回对应的帖子内容列表；若获取失败，则返回 None
        """
        if limit > 1000:  # 事实上，超过该值时，返回的结果会被截断到该值
            limit = 1000
            logger.warning(
                f"Limit is set to {limit}, Because it exceeds the maximum allowed value of 1000."
            )

        url = "/post.json"
        params = {
            "limit": limit,  # 您想检索多少篇帖子。每次请求的帖子数量有一个硬性限制，最多 1000 篇
            "page": 1,  # 查询页码
            "tags": tags,  # 要搜索的标签。任何在网站上有效的标签组合在这里都有效。这包括所有元标签。要组合的不同标签使用空格连接，同一标签中的空格使用 _ 替换
        }

        # 获取当前查询标签下所有页码的帖子列表
        if all_page:
            gt_page = await self.list_gt_page(  # 获取 html 分页器中的最大页码
                limit=limit,
                tags=tags,
            )
            logger.info(
                f"Maximum page number is greater than or equal to {gt_page} for {limit = }, {tags = }"
            )

            async for res in self.client.concurrent_fetch_page(
                url,
                params=params,
                start_page=1,
                end_page=gt_page,
                page_key="page",
            ):
                yield res

            #!仅适用于 posts 页面
            #!为防止遗漏帖子列表，回退至非并发模式获取 html 分页器中的最大页码之后的帖子列表
            logger.info(
                f"Fetching posts after {gt_page} page for {limit = }, {tags = }"
            )

            # 当前查询页码
            page = gt_page + 1
            # 直到获取到空数据为止
            while True:
                params.update({"page": page})
                content: list[dict] = await self.client.fetch_page(
                    url,
                    params=params,
                )
                if content:
                    yield content
                    page += 1
                else:
                    break

        # 获取在起始页码与结束页码范围内，指定标签的帖子列表
        else:
            async for res in self.client.concurrent_fetch_page(
                url,
                params=params,
                start_page=start_page,
                end_page=end_page,
                page_key="page",
            ):
                yield res

    def create(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def update(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def destroy(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def revert_tags(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def vote(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    async def download(
        self,
        limit: int = 40,
        start_page: int = 1,
        end_page: int = 1,
        all_page: bool = False,
        tags: str = "",
        save_raws: bool = False,
        save_tags: bool = False,
        overwrite: bool = False,
    ) -> None:
        """
        下载在起始页码与结束页码范围内，指定标签的帖子列表中的帖子；若 all_page 为 True，则下载当前查询标签下所有页码的帖子列表中的帖子

        Args:
            limit (int, optional): 您想检索多少篇帖子。每次请求的帖子数量有一个硬性限制，最多 1000 篇. Defaults to 40.
            start_page (int, optional): 查询起始页码. Defaults to 1.
            end_page (int, optional): 查询结束页码. Defaults to 1.
            all_page (bool, optional): 是否获取当前查询标签下所有页码的帖子列表，若为 True，则忽略 start_page 与 end_page 参数. Defaults to False.
            tags (str, optional): 要搜索的标签。任何在网站上有效的标签组合在这里都有效。这包括所有元标签。要组合的不同标签使用空格连接，同一标签中的空格使用 _ 替换. Defaults to ''. 表示搜索全站
            save_raws (bool, optional): 是否保存帖子 api 响应的元数据（json 格式）. Defaults to False.
            save_tags (bool, optional): 是否保存帖子标签. Defaults to False.
            overwrite (bool, optional): 是否覆盖已有同名文件. Defaults to False.
        """
        # 获取当前查询标签下所有页码的帖子列表中的帖子
        async for i, posts in aenumerate(
            self.list(
                limit=limit,
                start_page=start_page,
                end_page=end_page,
                all_page=all_page,
                tags=tags,
            )
        ):
            posts = pd.DataFrame(posts)

            if posts.empty:
                logger.info(f"All of the posts {i + 1} are empty.")
                continue

            # 下载帖子
            urls = posts["file_url"]  # 帖子 URLs
            posts_directory = os.path.join(
                self.directory, f"{tags if tags != '' else 'all'}"
            )  # 帖子文件目录
            images_directory = os.path.join(posts_directory, "images")  # 图像文件目录

            success_count, failure_count = 0, 0
            async for index, res in aenumerate(
                self.client.concurrent_download_file(
                    urls,
                    images_directory,
                )
            ):
                if res is None:
                    failure_count += 1
                    continue
                else:
                    success_count += 1

                url, filepath = res

                # 保存帖子 api 响应的元数据（json 格式）
                if save_raws:
                    # 保存元数据
                    raws = posts.loc[[index]]  # 筛选后的元数据
                    raws_directory = os.path.join(
                        posts_directory, "raws"
                    )  # 元数据文件目录
                    raws_filename = (
                        os.path.splitext(os.path.basename(filepath))[0] + ".json"
                    )  # 元数据文件名
                    await self.client.save_raws(
                        raws,
                        directory=raws_directory,
                        filename=raws_filename,
                        overwrite=overwrite,
                    )

                # 保存标签
                if save_tags:
                    # 帖子标签
                    tags = posts.at[index, "tags"]  # 筛选后的 tags
                    tags_directory = os.path.join(
                        posts_directory, "tags"
                    )  # 标签文件目录
                    tags_filename = (
                        os.path.splitext(os.path.basename(filepath))[0] + ".txt"
                    )  # 标签文件名
                    await self.client.save_tags(
                        tags,
                        directory=tags_directory,
                        filename=tags_filename,
                        overwrite=overwrite,
                    )

            logger.info(
                f"Downloaded {success_count} successful, {failure_count} failed for posts: {posts['id'].tolist()}"
            )


class YandereTags(MoebooruComponent):
    """
    Tags: https://yande.re/help/api#tags
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    def list(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def update(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def related(self):
        # TODO
        raise NotImplementedError("The method is not implemented")


class YandereArtists(MoebooruComponent):
    """
    Artists: https://yande.re/help/api#artists
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    def list(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def create(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def update(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def destroy(self):
        # TODO
        raise NotImplementedError("The method is not implemented")


class YandereComments(MoebooruComponent):
    """
    Comments: https://yande.re/help/api#comments
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    def show(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def create(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def destory(self):
        # TODO
        raise NotImplementedError("The method is not implemented")


class YandereWiki(MoebooruComponent):
    """
    Wiki: https://yande.re/help/api#wiki
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    def list(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def create(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def update(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def show(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def destroy(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def lock(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def unlock(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def revert(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def history(self):
        # TODO
        raise NotImplementedError("The method is not implemented")


class YandereNotes(MoebooruComponent):
    """
    Notes: https://yande.re/help/api#notes
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    def list(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def search(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def history(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def revert(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def create(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def update(self):
        # TODO
        raise NotImplementedError("The method is not implemented")


class YandereUsers(MoebooruComponent):
    """
    Users: https://yande.re/help/api#users
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    def search(self):
        # TODO
        raise NotImplementedError("The method is not implemented")


class YandereForum(MoebooruComponent):
    """
    Forum: https://yande.re/help/api#forum
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    def list(self):
        # TODO
        raise NotImplementedError("The method is not implemented")


class YanderePools(MoebooruComponent):
    """
    Pools: https://yande.re/help/api#pools
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    async def list_pools_page(
        self,
        query: str = "",
    ) -> int:
        """
        使用定位 html 分页器的方式，获取指定查询标题图集的最大页码

        Note:
            对于 pool 页面，由于不存在 Hidden Posts 策略（rating:e, blacklists .etc），实际的最大页码会等于该页码
            yande.re/pool 页面受 page 参数影响，但不受 limit 参数影响

        Args:
            query (str, optional): 查询标题. Defaults to ''. 表示搜索全站

        Returns:
            int: html 分页器中的最大页码，实际的最大页码等于该页码
        """
        url = "/pool"
        params = {
            "query": query,  # 查询标题
            "page": 1,  # 查询页码
        }

        try:
            response = await self.client.get(url, params=params)
            # 解析 html 分页器中的最大页码
            tree = etree.HTML(response.text)
            # 形如 ['2', '3', '4', '5', '1067', '1068', 'Next →'] 的样式。列表中的最后一个永远为 'Next →'；由于请求的 url 中的 page 参数固定为 1，当前页码信息 1 使用 em 标签而非 a 标签，故列表若存在，则永远以 2 开头
            pagination = tree.xpath('//div[@class="pagination"]/a[@aria-label]/text()')
            if pagination:  # 存在分页器，说明该页面至少有两页
                return int(pagination[-2])
            else:  # 不存在分页器，说明该页面只有一页
                return 1
        except httpx.HTTPError as exc:
            logger.error(f"{exc.__class__.__name__} for {exc.request.url} - {exc}")

    async def list_pools(
        self,
        query: str = "",
        start_page: int = 1,
        end_page: int = 1,
        all_page: bool = False,
    ) -> AsyncIterable[list[dict] | None]:
        """
        List Pools

        The base URL is /pool.xml. If you don't specify any parameters you'll get a list of all pools.

        - query: The title.
        - page: The page.

        Return json format:
        ```
        [
            {
                "id": 1746,
                "name": "K-ON!_-_Colorful_Memories",
                "created_at": "2010-07-18T00:21:15.758Z",
                "updated_at": "2012-03-11T19:10:27.707Z",
                "user_id": 3048,
                "is_public": true,
                "post_count": 44,
                "description": "http://kyotoanimation.shop-pro.jp/?pid=20190414"
            },
            ...
        ]
        ```

        获取在起始页码与结束页码范围内，指定标题的图集列表；若 all_page 为 True，则获取当前查询标题下所有页码的图集列表

        Note:
            yande.re/pool 接口受 page 参数影响，但不受 limit 参数影响

        Args:
            query (str, optional): 查询标题. Defaults to ''. 表示搜索全站
            start_page (int, optional): 查询起始页码. Defaults to 1.
            end_page (int, optional): 查询结束页码. Defaults to 1.
            all_page (bool, optional): 是否获取当前查询标题下所有页码的图集列表，若为 True，则忽略 start_page 与 end_page 参数. Defaults to False.

        Yields:
            AsyncIterable[list[dict] | None]: 若获取成功，则返回对应的图集内容列表；若获取失败，则返回 None
        """
        url = "/pool.json"
        params = {
            "query": query,  # 查询标题
            "page": 1,  # 查询页码
        }

        # 获取当前查询标题下所有页码的图集列表
        if all_page:
            max_page = await self.list_pools_page(
                query=query,
            )  # 获取 html 分页器中的最大页码
            logger.info(f"Maximum page number is equal to {max_page} for {query = }")

            async for res in self.client.concurrent_fetch_page(
                url,
                params=params,
                start_page=1,
                end_page=max_page,
                page_key="page",
            ):
                yield res

        # 获取在起始页码与结束页码范围内，指定标题的图集列表
        else:
            async for res in self.client.concurrent_fetch_page(
                url,
                params=params,
                start_page=start_page,
                end_page=end_page,
                page_key="page",
            ):
                yield res

    async def list_posts(
        self,
        id: int,
    ) -> list[dict] | None:
        """
        List Posts

        The base URL is /pool/show.xml. If you don't specify any parameters you'll get a list of all pools.

        Note:
            - 修订 API 参考文件: https://yande.re/help/api: If you don't specify any parameters you'll get a list of all pools. 将其更改为：If you don't specify any parameters you'll get a list of pool which id is 0.
            - id 参数是必须的，否则访问 https://yande.re/pool/show 或 https://yande.re/pool/show.json 是会自动跳转回 https://yande.re/pool/ 页面，并弹出 Can't find pool with id 0 提示
            - page 参数是可选的，pool/show 页面默认不以分页策略展示，所有内容均在一页中展示（忽略 limit 参数），设置为 1 即可。若需要分页策略，需点击页面最下方的 "Index View" 按钮，对于 id 为 1746 的图集，将跳转至 /post?tags=pool%3A1746 访问（即 /post?tags=pool:1746）
            - 图集支持批量下载，需点击页面最下方的 "Download" 按钮，对于 id 为 1746 的图集，将跳转至 /pool/zip/1746 访问，但需要用户登录后才能下载

        - id: The pool id number.
        - page: The page.

        Return json format:
        ```
        {
            "id": 1746,
            "name": "K-ON!_-_Colorful_Memories",
            "created_at": "2010-07-18T00:21:15.758Z",
            "updated_at": "2012-03-11T19:10:27.707Z",
            "user_id": 3048,
            "is_public": true,
            "post_count": 44,
            "description": "http://kyotoanimation.shop-pro.jp/?pid=20190414",
            "posts": [
                {
                    "id": 145519,
                    "tags": "akiyama_mio hirasawa_yui jpeg_artifacts k-on! kotobuki_tsumugi nakano_azusa pantyhose tainaka_ritsu",
                    "created_at": "2010-07-18T00:23:44.162Z",
                    "creator_id": 17990,
                    "author": "Share",
                    "change": 650971,
                    "source": "",
                    "score": 30,
                    "md5": "841ac093c4e6de2dd13ce1fb52703da7",
                    "file_size": 1143459,
                    "file_url": "https://files.yande.re/image/841ac093c4e6de2dd13ce1fb52703da7/yande.re%20145519%20akiyama_mio%20hirasawa_yui%20jpeg_artifacts%20k-on%21%20kotobuki_tsumugi%20nakano_azusa%20pantyhose%20tainaka_ritsu.jpg",
                    "is_shown_in_index": true,
                    "preview_url": "https://assets.yande.re/data/preview/84/1a/841ac093c4e6de2dd13ce1fb52703da7.jpg",
                    "preview_width": 106,
                    "preview_height": 150,
                    "actual_preview_width": 213,
                    "actual_preview_height": 300,
                    "sample_url": "https://files.yande.re/sample/841ac093c4e6de2dd13ce1fb52703da7/yande.re%20145519%20sample%20akiyama_mio%20hirasawa_yui%20jpeg_artifacts%20k-on%21%20kotobuki_tsumugi%20nakano_azusa%20pantyhose%20tainaka_ritsu.jpg",
                    "sample_width": 1065,
                    "sample_height": 1500,
                    "sample_file_size": 271128,
                    "jpeg_url": "https://files.yande.re/image/841ac093c4e6de2dd13ce1fb52703da7/yande.re%20145519%20akiyama_mio%20hirasawa_yui%20jpeg_artifacts%20k-on%21%20kotobuki_tsumugi%20nakano_azusa%20pantyhose%20tainaka_ritsu.jpg",
                    "jpeg_width": 3000,
                    "jpeg_height": 4226,
                    "jpeg_file_size": 0,
                    "rating": "s",
                    "has_children": false,
                    "parent_id": null,
                    "status": "active",
                    "width": 3000,
                    "height": 4226,
                    "is_held": false,
                    "frames_pending_string": "",
                    "frames_pending": [],
                    "frames_string": "",
                    "frames": []
                },
                ...
            ]
        }
        ```

        获取指定 id 的图集列表

        Args:
            id (int): 图集 id

        Returns:
            list[dict] | None: 若获取成功，则返回对应的图集内容列表；若获取失败，则返回 None
        """
        url = "/pool/show.json"
        params = {
            "id": id,  # 图集的 ID 号码
            "page": 1,  # 查询页码
        }

        # 结果列表
        result: list[dict] = await self.client.fetch_page(
            url,
            params=params,
            callback=lambda x: x.get("posts", []),  # 获取帖子列表
        )
        return result

    def update_pool(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def create_pool(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def destroy_pool(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def add_post(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    def remove_post(self):
        # TODO
        raise NotImplementedError("The method is not implemented")

    async def download(
        self,
        query: str = "",
        start_page: int = 1,
        end_page: int = 1,
        all_page: bool = False,
        save_raws: bool = False,
        save_tags: bool = False,
        overwrite: bool = False,
    ) -> None:
        """
        下载在起始页码与结束页码范围内，指定标题的图集列表中的帖子；若 all_page 为 True，则下载当前查询标题下所有页码的图集列表中的帖子

        Args:
            query (str, optional): 查询标题. Defaults to ''. 表示搜索全站
            start_page (int, optional): 查询起始页码. Defaults to 1.
            end_page (int, optional): 查询结束页码. Defaults to 1.
            all_page (bool, optional): 是否下载当前查询标题下所有页码的图集列表中的帖子，若为 True，则忽略 start_page 与 end_page 参数. Defaults to False.
            save_raws (bool, optional): 是否保存帖子 api 响应的元数据（json 格式）. Defaults to False.
            save_tags (bool, optional): 是否保存帖子标签. Defaults to False.
            overwrite (bool, optional): 是否覆盖已有同名文件. Defaults to False.
        """
        # 获取当前查询标题下所有页码的图集列表
        async for i, pools in aenumerate(
            self.list_pools(
                query=query,
                start_page=start_page,
                end_page=end_page,
                all_page=all_page,
            )
        ):
            pools = pd.DataFrame(pools)

            if pools.empty:
                logger.info(f"All of the pools {i + 1} are empty.")
                continue

            # 图集 id
            ids = pools["id"]
            # 图集名称
            names = pools["name"]

            # 遍历图集列表
            for id, name in zip(ids, names):
                # 获取图集 ID 下所有帖子
                posts = await self.list_posts(
                    id=id,
                )
                posts = pd.DataFrame(posts)

                # 下载帖子
                urls = posts["file_url"]  # 帖子 URLs
                posts_directory = os.path.join(
                    self.directory, f"{name}"
                )  # 帖子文件目录
                images_directory = os.path.join(
                    posts_directory, "images"
                )  # 图像文件目录

                success_count, failure_count = 0, 0
                async for index, res in aenumerate(
                    self.client.concurrent_download_file(
                        urls,
                        images_directory,
                    )
                ):
                    if res is None:
                        failure_count += 1
                        continue
                    else:
                        success_count += 1
                    url, filepath = res

                    # 保存帖子 api 响应的元数据（json 格式）
                    if save_raws:
                        # 保存元数据
                        raws = posts.loc[[index]]  # 筛选后的元数据
                        raws_directory = os.path.join(
                            posts_directory, "raws"
                        )  # 元数据文件目录
                        raws_filename = (
                            os.path.splitext(os.path.basename(filepath))[0] + ".json"
                        )  # 元数据文件名
                        await self.client.save_raws(
                            raws,
                            directory=raws_directory,
                            filename=raws_filename,
                            overwrite=overwrite,
                        )

                    # 保存标签
                    if save_tags:
                        # 帖子标签
                        tags = posts.at[index, "tags"]  # 筛选后的 tags
                        tags_directory = os.path.join(
                            posts_directory, "tags"
                        )  # 标签文件目录
                        tags_filename = (
                            os.path.splitext(os.path.basename(filepath))[0] + ".txt"
                        )  # 标签文件名
                        await self.client.save_tags(
                            tags,
                            directory=tags_directory,
                            filename=tags_filename,
                            overwrite=overwrite,
                        )

                logger.info(
                    f"Downloaded {success_count} successful, {failure_count} failed for pool: {name}"
                )


class YandereFavorites(MoebooruComponent):
    """
    Favorites: https://yande.re/help/api#favorites
    """

    def __init__(self, client: YandereClient):
        super().__init__(client)

    def list_users(self):
        # TODO
        raise NotImplementedError("The method is not implemented")
