# -*- coding: utf-8 -*-
import requests
from deprecated import deprecated

from .session import Requestor
from .formats import JSON, LIJSON, PGN, NDJSON
from . import models


__all__ = [
    'Client',
    'Account',
    'Board',
    'Bots',
    'Broadcasts',
    'Challenges',
    'Games',
    'Simuls',
    'Studies',
    'Teams',
    'Tournaments',
    'Users',
]


# Base URL for the API
API_URL = 'https://lichess.org/'


class BaseClient:

    def __init__(self, session, base_url=None):
        self._r = Requestor(session, base_url or API_URL, default_fmt=JSON)


class FmtClient(BaseClient):
    """Client that can return PGN or not.

    :param session: request session, authenticated as needed
    :type session: :class:`requests.Session`
    :param str base_url: base URL for the API
    :param bool pgn_as_default: ``True`` if PGN should be the default format
                                for game exports when possible. This defaults
                                to ``False`` and is used as a fallback when
                                ``as_pgn`` is left as ``None`` for methods that
                                support it.
    """

    def __init__(self, session, base_url=None, pgn_as_default=False):
        super().__init__(session, base_url)
        self.pgn_as_default = pgn_as_default

    def _use_pgn(self, as_pgn=None):
        # helper to merge default with provided arg
        return as_pgn if as_pgn is not None else self.pgn_as_default


class Client(BaseClient):
    """Main touchpoint for the API.

    All endpoints are namespaced into the clients below:

    - :class:`account <berserk.clients.Account>` - managing account information
    - :class:`bots <berserk.clients.Bots>` - performing bot operations
    - :class:`broadcasts <berserk.clients.Broadcasts>` - getting and creating
      broadcasts
    - :class:`challenges <berserk.clients.Challenges>` - using challenges
    - :class:`games <berserk.clients.Games>` - getting and exporting games
    - :class:`simuls <berserk.clients.Simuls>` - getting simultaneous
      exhibition games
    - :class:`studies <berserk.clients.Studies>` - exporting studies
    - :class:`teams <berserk.clients.Teams>` - getting information about teams
    - :class:`tournaments <berserk.clients.Tournaments>` - getting and
      creating tournaments
    - :class:`users <berserk.clients.Users>` - getting information about users

    :param session: request session, authenticated as needed
    :type session: :class:`requests.Session`
    :param str base_url: base API URL to use (if other than the default)
    :param bool pgn_as_default: ``True`` if PGN should be the default format
                                for game exports when possible. This defaults
                                to ``False`` and is used as a fallback when
                                ``as_pgn`` is left as ``None`` for methods that
                                support it.
    """

    def __init__(self, session=None, base_url=None, pgn_as_default=False):
        session = session or requests.Session()
        super().__init__(session, base_url)
        self.account = Account(session, base_url)
        self.users = Users(session, base_url)
        self.teams = Teams(session, base_url)
        self.games = Games(session, base_url, pgn_as_default=pgn_as_default)
        self.challenges = Challenges(session, base_url)
        self.board = Board(session, base_url)
        self.bots = Bots(session, base_url)
        self.tournaments = Tournaments(session, base_url,
                                       pgn_as_default=pgn_as_default)
        self.broadcasts = Broadcasts(session, base_url)
        self.simuls = Simuls(session, base_url)
        self.studies = Studies(session, base_url)


class Account(BaseClient):
    """Client for account-related endpoints."""

    def get(self):
        """Get your public information.

        :return: public information about the authenticated user
        :rtype: dict
        """
        path = 'api/account'
        return self._r.get(path, converter=models.Account.convert)

    def get_email(self):
        """Get your email address.

        :return: email address of the authenticated user
        :rtype: str
        """
        path = 'api/account/email'
        return self._r.get(path)['email']

    def get_preferences(self):
        """Get your account preferences.

        :return: preferences of the authenticated user
        :rtype: dict
        """
        path = 'api/account/preferences'
        return self._r.get(path)['prefs']

    def get_kid_mode(self):
        """Get your kid mode status.

        :return: current kid mode status
        :rtype: bool
        """
        path = 'api/account/kid'
        return self._r.get(path)['kid']

    def set_kid_mode(self, value):
        """Set your kid mode status.

        :param bool value: whether to enable or disable kid mode
        :return: success
        :rtype: bool
        """
        path = 'api/account/kid'
        params = {'v': value}
        return self._r.post(path, params=params)['ok']

    def upgrade_to_bot(self):
        """Upgrade your account to a bot account.

        Requires bot:play oauth scope. User cannot have any previously played
        games.

        :return: success
        :rtype: bool
        """
        path = 'api/bot/account/upgrade'
        return self._r.post(path)['ok']


class Users(BaseClient):
    """Client for user-related endpoints."""

    def get_puzzle_activity(self, max=None):
        """Stream puzzle activity history starting with the most recent.

        :param int max: maximum number of entries to stream
        :return: puzzle activity history
        :rtype: iter
        """
        path = 'api/user/puzzle-activity'
        params = {'max': max}
        return self._r.get(path, params=params, fmt=NDJSON, stream=True,
                           converter=models.PuzzleActivity.convert)

    def get_realtime_statuses(self, *user_ids):
        """Get the online, playing, and streaming statuses of players.

        Only id and name fields are returned for offline users.

        :param user_ids: one or more user IDs (names)
        :return: statuses of given players
        :rtype: list
        """
        path = 'api/users/status'
        params = {'ids': ','.join(user_ids)}
        return self._r.get(path, params=params)

    def get_all_top_10(self):
        """Get the top 10 players for each speed and variant.

        :return: top 10 players in each speed and variant
        :rtype: dict
        """
        path = 'player'
        return self._r.get(path, fmt=LIJSON)

    def get_leaderboard(self, perf_type, count=10):
        """Get the leaderboard for one speed or variant.

        :param perf_type: speed or variant
        :type perf_type: :class:`~berserk.enums.PerfType`
        :param int count: number of players to get
        :return: top players for one speed or variant
        :rtype: list
        """
        path = f'player/top/{count}/{perf_type}'
        return self._r.get(path, fmt=LIJSON)['users']

    def get_public_data(self, username):
        """Get the public data for a user.

        :param str username: username
        :return: public data available for the given user
        :rtype: dict
        """
        path = f'api/user/{username}'
        return self._r.get(path, converter=models.User.convert)

    def get_activity_feed(self, username):
        """Get the activity feed of a user.

        :param str username: username
        :return: activity feed of the given user
        :rtype: list
        """
        path = f'api/user/{username}/activity'
        return self._r.get(path, converter=models.Activity.convert)

    def get_by_id(self, *usernames):
        """Get multiple users by their IDs.

        :param usernames: one or more usernames
        :return: user data for the given usernames
        :rtype: list
        """
        path = 'api/users'
        return self._r.post(path, data=','.join(usernames),
                            converter=models.User.convert)

    @deprecated(version='0.7.0', reason='use Teams.get_members(id) instead')
    def get_by_team(self, team_id):
        """Get members of a team.

        :param str team_id: ID of a team
        :return: users on the given team
        :rtype: iter
        """
        path = f'team/{team_id}/users'
        return self._r.get(path, fmt=NDJSON, stream=True,
                           converter=models.User.convert)

    def get_live_streamers(self):
        """Get basic information about currently streaming users.

        :return: users currently streaming a game
        :rtype: list
        """
        path = 'streamer/live'
        return self._r.get(path)

    def get_users_followed(self, username):
        """Stream users followed by a user.

        :param str username: a username
        :return: iterator over the users the given user follows
        :rtype: iter
        """
        path = f'/api/user/{username}/following'
        return self._r.get(path, stream=True, fmt=NDJSON,
                           converter=models.User.convert)

    def get_users_following(self, username):
        """Stream users who follow a user.

        :param str username: a username
        :return: iterator over the users that follow the given user
        :rtype: iter
        """
        path = f'/api/user/{username}/followers'
        return self._r.get(path, stream=True, fmt=NDJSON,
                           converter=models.User.convert)

    def get_rating_history(self, username):
        """Get the rating history of a user.

        :param str username: a username
        :return: rating history for all game types
        :rtype: list
        """
        path = f'/api/user/{username}/rating-history'
        return self._r.get(path, converter=models.RatingHistory.convert)


class Teams(BaseClient):

    def get_members(self, team_id):
        """Get members of a team.

        :param str team_id: ID of a team
        :return: users on the given team
        :rtype: iter
        """
        path = f'team/{team_id}/users'
        return self._r.get(path, fmt=NDJSON, stream=True,
                           converter=models.User.convert)

    def join(self, team_id):
        """Join a team.

        :param str team_id: ID of a team
        :return: success
        :rtype: bool
        """
        path = f'/team/{team_id}/join'
        return self._r.post(path)['ok']

    def leave(self, team_id):
        """Leave a team.

        :param str team_id: ID of a team
        :return: success
        :rtype: bool
        """
        path = f'/team/{team_id}/quit'
        return self._r.post(path)['ok']

    def kick_member(self, team_id, user_id):
        """Kick a member out of your team.

        :param str team_id: ID of a team
        :param str user_id: ID of a team member
        :return: success
        :rtype: bool
        """
        path = f'/team/{team_id}/kick/{user_id}'
        return self._r.post(path)['ok']


class Games(FmtClient):
    """Client for games-related endpoints."""

    def export(self, game_id, as_pgn=None, moves=None, tags=None, clocks=None,
               evals=None, opening=None, literate=None):
        """Get one finished game as PGN or JSON.

        :param str game_id: the ID of the game to export
        :param bool as_pgn: whether to return the game in PGN format
        :param bool moves: whether to include the PGN moves
        :param bool tags: whether to include the PGN tags
        :param bool clocks: whether to include clock comments in the PGN moves
        :param bool evals: whether to include analysis evaluation comments in
                           the PGN moves when available
        :param bool opening: whether to include the opening name
        :param bool literate: whether to include literate the PGN
        :return: exported game, as JSON or PGN
        """
        path = f'game/export/{game_id}'
        params = {
            'moves': moves,
            'tags': tags,
            'clocks': clocks,
            'evals': evals,
            'opening': opening,
            'literate': literate,
        }
        fmt = PGN if self._use_pgn(as_pgn) else JSON
        return self._r.get(path, params=params, fmt=fmt,
                           converter=models.Game.convert)

    def export_by_player(self, username, as_pgn=None, since=None, until=None,
                         max=None, vs=None, rated=None, perf_type=None,
                         color=None, analysed=None, moves=None, tags=None,
                         evals=None, opening=None):
        """Get games by player.

        :param str username: which player's games to return
        :param bool as_pgn: whether to return the game in PGN format
        :param int since: lowerbound on the game timestamp
        :param int until: upperbound on the game timestamp
        :param int max: limit the number of games returned
        :param str vs: filter by username of the opponent
        :param bool rated: filter by game mode (``True`` for rated, ``False``
                           for casual)
        :param perf_type: filter by speed or variant
        :type perf_type: :class:`~berserk.enums.PerfType`
        :param color: filter by the color of the player
        :type color: :class:`~berserk.enums.Color`
        :param bool analysed: filter by analysis availability
        :param bool moves: whether to include the PGN moves
        :param bool tags: whether to include the PGN tags
        :param bool clocks: whether to include clock comments in the PGN moves
        :param bool evals: whether to include analysis evaluation comments in
                           the PGN moves when available
        :param bool opening: whether to include the opening name
        :param bool literate: whether to include literate the PGN
        :return: iterator over the exported games, as JSON or PGN
        """
        path = f'api/games/user/{username}'
        params = {
            'since': since,
            'until': until,
            'max': max,
            'vs': vs,
            'rated': rated,
            'perfType': perf_type,
            'color': color,
            'analysed': analysed,
            'moves': moves,
            'tags': tags,
            'evals': evals,
            'opening': opening,
        }
        fmt = PGN if self._use_pgn(as_pgn) else NDJSON
        yield from self._r.get(path, params=params, fmt=fmt, stream=True,
                               converter=models.Game.convert)

    def export_multi(self, *game_ids, as_pgn=None, moves=None, tags=None,
                     clocks=None, evals=None, opening=None):
        """Get multiple games by ID.

        :param game_ids: one or more game IDs to export
        :param bool as_pgn: whether to return the game in PGN format
        :param bool moves: whether to include the PGN moves
        :param bool tags: whether to include the PGN tags
        :param bool clocks: whether to include clock comments in the PGN moves
        :param bool evals: whether to include analysis evaluation comments in
                           the PGN moves when available
        :param bool opening: whether to include the opening name
        :return: iterator over the exported games, as JSON or PGN
        """
        path = 'games/export/_ids'
        params = {
            'moves': moves,
            'tags': tags,
            'clocks': clocks,
            'evals': evals,
            'opening': opening,
        }
        payload = ','.join(game_ids)
        fmt = PGN if self._use_pgn(as_pgn) else NDJSON
        yield from self._r.post(path, params=params, data=payload, fmt=fmt,
                                stream=True, converter=models.Game.convert)

    def get_among_players(self, *usernames):
        """Get the games currently being played among players.

        Note this will not includes games where only one player is in the given
        list of usernames.

        :param usernames: two or more usernames
        :return: iterator over all games played among the given players
        """
        path = 'api/stream/games-by-users'
        payload = ','.join(usernames)
        yield from self._r.post(path, data=payload, fmt=NDJSON, stream=True,
                                converter=models.Game.convert)

    # move this to Account?
    def get_ongoing(self, count=10):
        """Get your currently ongoing games.

        :param int count: number of games to get
        :return: some number of currently ongoing games
        :rtype: list
        """
        path = 'api/account/playing'
        params = {'nb': count}
        return self._r.get(path, params=params)['nowPlaying']

    def get_tv_channels(self):
        """Get basic information about the best games being played.

        :return: best ongoing games in each speed and variant
        :rtype: dict
        """
        path = 'tv/channels'
        return self._r.get(path)


class Challenges(BaseClient):

    def create(self, username, rated, clock_limit=None, clock_increment=None,
               days=None, color=None, variant=None, position=None):
        """Challenge another player to a game.

        :param str username: username of the player to challege
        :param bool rated: whether or not the game will be rated
        :param int clock_limit: clock initial time (in seconds)
        :param int clock_increment: clock increment (in seconds)
        :param int days: days per move (for correspondence games; omit clock)
        :param color: color of the accepting player
        :type color: :class:`~berserk.enums.Color`
        :param variant: game variant to use
        :type variant: :class:`~berserk.enums.Variant`
        :param position: use one of the custom initial positions (cannot be a
                         rated game)
        :type position: :class:`~berserk.enums.Position`
        :return: success indicator
        :rtype: bool
        """
        path = f'api/challenge/{username}'
        payload = {
            'username': username,
            'rated': rated,
            'clock.limit': clock_limit,
            'clock.increment': clock_increment,
            'days': days,
            'color': color,
            'variant': variant,
            'position': position,
        }
        return self._r.post(path, json=payload,
                            converter=models.Tournament.convert)

    def accept(self, challenge_id):
        """Accept an incoming challenge.

        :param str challenge_id: id of the challenge to accept
        :return: success indicator
        :rtype: bool
        """
        path = f'api/challenge/{challenge_id}/accept'
        return self._r.post(path)['ok']

    def decline(self, challenge_id):
        """Decline an incoming challenge.

        :param str challenge_id: id of the challenge to decline
        :return: success indicator
        :rtype: bool
        """
        path = f'api/challenge/{challenge_id}/decline'
        return self._r.post(path)['ok']


class Board(BaseClient):
    """Client for physical board or external application endpoints."""

    def stream_incoming_events(self):
        """Get your realtime stream of incoming events.

        :return: stream of incoming events
        :rtype: iterator over the stream of events
        """
        path = 'api/stream/event'
        yield from self._r.get(path, stream=True)

    def stream_game_state(self, game_id):
        """Get the stream of events for a board game.

        :param str game_id: ID of a game
        :return: iterator over game states
        """
        path = f'api/board/game/stream/{game_id}'
        yield from self._r.get(path, stream=True,
                               converter=models.GameState.convert)

    def make_move(self, game_id, move):
        """Make a move in a board game.

        :param str game_id: ID of a game
        :param str move: move to make
        :return: success
        :rtype: bool
        """
        path = f'api/board/game/{game_id}/move/{move}'
        return self._r.post(path)['ok']

    def post_message(self, game_id, text, spectator=False):
        """Post a message in a board game.

        :param str game_id: ID of a game
        :param str text: text of the message
        :param bool spectator: post to spectator room (else player room)
        :return: success
        :rtype: bool
        """
        path = f'api/board/game/{game_id}/chat'
        room = 'spectator' if spectator else 'player'
        payload = {'room': room, 'text': text}
        return self._r.post(path, json=payload)['ok']

    def abort_game(self, game_id):
        """Abort a board game.

        :param str game_id: ID of a game
        :return: success
        :rtype: bool
        """
        path = f'api/board/game/{game_id}/abort'
        return self._r.post(path)['ok']

    def resign_game(self, game_id):
        """Resign a board game.

        :param str game_id: ID of a game
        :return: success
        :rtype: bool
        """
        path = f'api/board/game/{game_id}/resign'
        return self._r.post(path)['ok']


class Bots(BaseClient):
    """Client for bot-related endpoints."""

    def stream_incoming_events(self):
        """Get your realtime stream of incoming events.

        :return: stream of incoming events
        :rtype: iterator over the stream of events
        """
        path = 'api/stream/event'
        yield from self._r.get(path, stream=True)

    def stream_game_state(self, game_id):
        """Get the stream of events for a bot game.

        :param str game_id: ID of a game
        :return: iterator over game states
        """
        path = f'api/bot/game/stream/{game_id}'
        yield from self._r.get(path, stream=True,
                               converter=models.GameState.convert)

    def make_move(self, game_id, move):
        """Make a move in a bot game.

        :param str game_id: ID of a game
        :param str move: move to make
        :return: success
        :rtype: bool
        """
        path = f'api/bot/game/{game_id}/move/{move}'
        return self._r.post(path)['ok']

    def post_message(self, game_id, text, spectator=False):
        """Post a message in a bot game.

        :param str game_id: ID of a game
        :param str text: text of the message
        :param bool spectator: post to spectator room (else player room)
        :return: success
        :rtype: bool
        """
        path = f'api/bot/game/{game_id}/chat'
        room = 'spectator' if spectator else 'player'
        payload = {'room': room, 'text': text}
        return self._r.post(path, json=payload)['ok']

    def abort_game(self, game_id):
        """Abort a bot game.

        :param str game_id: ID of a game
        :return: success
        :rtype: bool
        """
        path = f'api/bot/game/{game_id}/abort'
        return self._r.post(path)['ok']

    def resign_game(self, game_id):
        """Resign a bot game.

        :param str game_id: ID of a game
        :return: success
        :rtype: bool
        """
        path = f'api/bot/game/{game_id}/resign'
        return self._r.post(path)['ok']

    def accept_challenge(self, challenge_id):
        """Accept an incoming challenge.

        :param str challenge_id: ID of a challenge
        :return: success
        :rtype: bool
        """
        path = f'api/challenge/{challenge_id}/accept'
        return self._r.post(path)['ok']

    def decline_challenge(self, challenge_id):
        """Decline an incoming challenge.

        :param str challenge_id: ID of a challenge
        :return: success
        :rtype: bool
        """
        path = f'api/challenge/{challenge_id}/decline'
        return self._r.post(path)['ok']


class Tournaments(FmtClient):
    """Client for tournament-related endpoints."""

    def get(self):
        """Get recently finished, ongoing, and upcoming tournaments.

        :return: current tournaments
        :rtype: list
        """
        path = 'api/tournament'
        return self._r.get(path, converter=models.Tournaments.convert_values)

    def create(self, clock_time, clock_increment, minutes, name=None,
               wait_minutes=None, variant=None, berserkable=None, rated=None,
               start_date=None, position=None, password=None, conditions=None):
        """Create a new tournament.

        .. note::

            ``wait_minutes`` is always relative to now and is overriden by
            ``start_time``.

        .. note::

            If ``name`` is left blank then one is automatically created.

        :param int clock_time: intial clock time in minutes
        :param int clock_increment: clock increment in seconds
        :param int minutes: length of the tournament in minutes
        :param str name: tournament name
        :param int wait_minutes: future start time in minutes
        :param str start_date: when to start the tournament
        :param str variant: variant to use if other than standard
        :param bool rated: whether the game affects player ratings
        :param str berserkable: whether players can use berserk
        :param str position: custom initial position in FEN
        :param str password: password (makes the tournament private)
        :param dict conditions: conditions for participation
        :return: created tournament info
        :rtype: dict
        """
        path = 'api/tournament'
        payload = {
            'name': name,
            'clockTime': clock_time,
            'clockIncrement': clock_increment,
            'minutes': minutes,
            'waitMinutes': wait_minutes,
            'startDate': start_date,
            'variant': variant,
            'rated': rated,
            'position': position,
            'berserkable': berserkable,
            'password': password,
            **{f'conditions.{c}': v for c, v in (conditions or {}).items()},
        }
        return self._r.post(path, json=payload,
                            converter=models.Tournament.convert)

    def export_games(self, id_, as_pgn=False, moves=None, tags=None,
                     clocks=None, evals=None, opening=None):
        """Export games from a tournament.

        :param str id_: tournament ID
        :param bool as_pgn: whether to return PGN instead of JSON
        :param bool moves: include moves
        :param bool tags: include tags
        :param bool clocks: include clock comments in the PGN moves, when
                            available
        :param bool evals: include analysis evalulation comments in the PGN
                           moves, when available
        :param bool opening: include the opening name
        :return: games
        :rtype: list
        """
        path = f'api/tournament/{id_}/games'
        params = {
            'moves': moves,
            'tags': tags,
            'clocks': clocks,
            'evals': evals,
            'opening': opening,
        }
        fmt = PGN if self._use_pgn(as_pgn) else NDJSON
        return self._r.get(path, params=params, fmt=fmt,
                           converter=models.Game.convert)

    def stream_results(self, id_, limit=None):
        """Stream the results of a tournament.

        Results are the players of a tournament with their scores and
        performance in rank order. Note that results for ongoing
        tournaments can be inconsistent due to ranking changes.

        :param str id_: tournament ID
        :param int limit: maximum number of results to stream
        :return: iterator over the stream of results
        :rtype: iter
        """
        path = f'api/tournament/{id_}/results'
        params = {'nb': limit}
        return self._r.get(path, params=params, stream=True)

    def stream_by_creator(self, username):
        """Stream the tournaments created by a player.

        :param str username: username of the player
        :return: tournaments
        :rtype: iter
        """
        path = f'api/user/{username}/tournament/created'
        return self._r.get(path, stream=True)


class Broadcasts(BaseClient):
    """Broadcast of one or more games."""

    def create(self, name, description, sync_url=None, markdown=None,
               credit=None, starts_at=None, official=None, throttle=None):
        """Create a new broadcast.

        .. note::

            ``sync_url`` must be publicly accessible. If not provided, you
            must periodically push new PGN to update the broadcast manually.

        :param str name: name of the broadcast
        :param str description: short description
        :param str markdown: long description
        :param str sync_url: URL by which Lichess can poll for updates
        :param str credit: short text to give credit to the source provider
        :param int starts_at: start time as millis
        :param bool official: DO NOT USE
        :param int throttle: DO NOT USE
        :return: created tournament info
        :rtype: dict
        """
        path = 'broadcast/new'
        payload = {
            'name': name,
            'description': description,
            'syncUrl': sync_url,
            'markdown': markdown,
            'credit': credit,
            'startsAt': starts_at,
            'official': official,
            'throttle': throttle,
        }
        return self._r.post(path, json=payload,
                            converter=models.Broadcast.convert)

    def get(self, broadcast_id, slug='-'):
        """Get a broadcast by ID.

        :param str broadcast_id: ID of a broadcast
        :param str slug: slug for SEO
        :return: broadcast information
        :rtype: dict
        """
        path = f'broadcast/{slug}/{broadcast_id}'
        return self._r.get(path, converter=models.Broadcast.convert)

    def update(self, broadcast_id, name, description, sync_url, markdown=None,
               credit=None, starts_at=None, official=None, throttle=None,
               slug='-'):
        """Update an existing broadcast by ID.

        .. note::

            Provide all fields. Values in missing fields will be erased.

        :param str broadcast_id: ID of a broadcast
        :param str name: name of the broadcast
        :param str description: short description
        :param str sync_url: URL by which Lichess can poll for updates
        :param str markdown: long description
        :param str credit: short text to give credit to the source provider
        :param int starts_at: start time as millis
        :param bool official: DO NOT USE
        :param int throttle: DO NOT USE
        :param str slug: slug for SEO
        :return: updated broadcast information
        :rtype: dict
        """
        path = f'broadcast/{slug}/{broadcast_id}'
        payload = {
            'name': name,
            'description': description,
            'syncUrl': sync_url,
            'markdown': markdown,
            'credit': credit,
            'startsAt': starts_at,
            'official': official,

        }
        return self._r.post(path, json=payload,
                            converter=models.Broadcast.convert)

    def push_pgn_update(self, broadcast_id, pgn_games, slug='-'):
        """Manually update an existing broadcast by ID.

        :param str broadcast_id: ID of a broadcast
        :param list pgn_games: one or more games in PGN format
        :return: success
        :rtype: bool
        """
        path = f'broadcast/{slug}/{broadcast_id}/push'
        games = '\n\n'.join(g.strip() for g in pgn_games)
        return self._r.post(path, data=games)['ok']


class Simuls(BaseClient):
    """Simultaneous exhibitions - one vs many."""

    def get(self):
        """Get recently finished, ongoing, and upcoming simuls.

        :return: current simuls
        :rtype: list
        """
        path = 'api/simul'
        return self._r.get(path)


class Studies(BaseClient):
    """Study chess the Lichess way."""

    def export_chapter(self, study_id, chapter_id):
        """Export one chapter of a study.

        :return: chapter
        :rtype: PGN
        """
        path = f'/study/{study_id}/{chapter_id}.pgn'
        return self._r.get(path, fmt=PGN)

    def export(self, study_id):
        """Export all chapters of a study.

        :return: all chapters as PGN
        :rtype: list
        """
        path = f'/study/{study_id}.pgn'
        return self._r.get(path, fmt=PGN, stream=True)
