# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for account functionality."""

# pylint: disable=too-many-lines

import logging
from typing import override

from bacommon.cloud import WebLocation
from bacommon.login import LoginType
import bacommon.cloud
import bauiv1 as bui
from bauiv1 import builtinassets
from bauiv1 import stdassets

from bauiv1lib.utils import scroll_fade_bottom, scroll_fade_top
from bauiv1lib.connectivity import wait_for_connectivity


class AccountSettingsWindow(bui.MainWindow):
    """Window for account related functionality."""

    def __init__(
        self,
        transition: str | None = 'in_right',
        origin_widget: bui.Widget | None = None,
        close_once_signed_in: bool = False,
        auxiliary_style: bool = True,
    ):

        plus = bui.app.plus
        assert plus is not None

        self._uiopenstate = bui.UIOpenState('accountsettings')

        self._sign_in_v2_proxy_button: bui.Widget | None = None
        self._sign_in_device_button: bui.Widget | None = None

        self._show_legacy_unlink_button = False

        self._signing_in_adapter: bui.LoginAdapter | None = None
        self._signing_in_discord: bool = False
        self._close_once_signed_in = close_once_signed_in
        bui.set_analytics_screen('Account Window')

        self._explicitly_signed_out_of_gpgs = False

        self._r = 'accountSettingsWindow'
        self._needs_refresh = False
        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
        self._v1_account_state_num = plus.get_v1_account_state_num()
        self._check_sign_in_timer = bui.AppTimer(
            1.0, bui.WeakCallStrict(self._update), repeat=True
        )

        self._can_reset_achievements = False
        self._recreate_suppress: bui.MainWindowAutoRecreateSuppress | None = (
            None
        )

        app = bui.app
        assert app.classic is not None
        uiscale = app.ui_v1.uiscale

        self._width = 980 if uiscale is bui.UIScale.SMALL else 660
        self._height = (
            600
            if uiscale is bui.UIScale.SMALL
            else 430 if uiscale is bui.UIScale.MEDIUM else 490
        )

        # Do some fancy math to fill all available screen area up to the
        # size of our backing container. This lets us fit to the exact
        # screen shape at small ui scale.
        screensize = bui.get_virtual_screen_size()

        scale = (
            1.9
            if uiscale is bui.UIScale.SMALL
            else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
        )
        # Calc screen size in our local container space and clamp to a
        # bit smaller than our container size.
        target_width = min(self._width - 80, screensize[0] / scale)
        target_height = min(self._height - 80, screensize[1] / scale)

        # To get top/left coords, go to the center of our window and
        # offset by half the width/height of our target area.
        yoffs = 0.5 * self._height + 0.5 * target_height + 30.0

        self._scroll_width = target_width
        self._scroll_height = target_height - 33
        scroll_bottom = yoffs - 61 - self._scroll_height

        # Go with full-screen scrollable area in small ui.
        if uiscale is bui.UIScale.SMALL:
            self._scroll_height += 35
            scroll_bottom -= 3

        self._sign_in_button = None
        self._sign_in_text = None

        self._sub_width = self._scroll_width - 20

        # Determine which sign-in/sign-out buttons we should show.
        self._show_sign_in_buttons: list[str] = []

        if LoginType.GPGS in plus.accounts.login_adapters:
            self._show_sign_in_buttons.append('Google Play')

        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
            self._show_sign_in_buttons.append('Game Center')

        if plus.accounts.discord_available:
            self._show_sign_in_buttons.append('Discord')

        # Always want to show our web-based v2 login option.
        self._show_sign_in_buttons.append('V2Proxy')

        # Legacy v1 device accounts available only if the user has
        # explicitly enabled deprecated login types.
        if bui.app.config.resolve('Show Deprecated Login Types'):
            self._show_sign_in_buttons.append('Device')

        super().__init__(
            root_widget=bui.containerwidget(
                size=(self._width, self._height),
                toolbar_visibility='menu_full',
                toolbar_cancel_button_style=(
                    'close' if auxiliary_style else 'back'
                ),
                scale=scale,
            ),
            transition=transition,
            origin_widget=origin_widget,
            # We're affected by screen size only at small ui-scale.
            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
        )
        if uiscale is bui.UIScale.SMALL:
            self._back_button = None
            bui.containerwidget(
                edit=self._root_widget, on_cancel_call=self.main_window_back
            )
        else:
            self._back_button = btn = bui.buttonwidget(
                parent=self._root_widget,
                id=f'{self.main_window_id_prefix}|back',
                position=(51, yoffs - 52.0),
                size=(60, 56),
                scale=0.8,
                text_scale=1.2,
                autoselect=True,
                button_type=None if auxiliary_style else 'backSmall',
                on_activate_call=self.main_window_back,
                label=bui.charstr(
                    bui.SpecialChar.CLOSE
                    if auxiliary_style
                    else bui.SpecialChar.BACK
                ),
            )
            bui.containerwidget(edit=self._root_widget, cancel_button=btn)

        self._scrollwidget = bui.scrollwidget(
            parent=self._root_widget,
            highlight=False,
            size=(self._scroll_width, self._scroll_height),
            position=(
                self._width * 0.5 - self._scroll_width * 0.5,
                scroll_bottom,
            ),
            claims_left_right=True,
            selection_loops_to_parent=True,
            border_opacity=0.4,
        )

        # With full-screen scrolling, fade content as it approaches
        # toolbars.
        if uiscale is bui.UIScale.SMALL:
            scroll_fade_top(
                self._root_widget,
                self._width * 0.5 - self._scroll_width * 0.5,
                scroll_bottom,
                self._scroll_width,
                self._scroll_height,
            )
            scroll_fade_bottom(
                self._root_widget,
                self._width * 0.5 - self._scroll_width * 0.5,
                scroll_bottom,
                self._scroll_width,
                self._scroll_height,
            )

        titleyoffs = -45.0 if uiscale is bui.UIScale.SMALL else -28.0
        titlescale = 0.7 if uiscale is bui.UIScale.SMALL else 1.0
        bui.textwidget(
            parent=self._root_widget,
            position=(
                self._width * 0.5,
                yoffs + titleyoffs,
            ),
            size=(0, 0),
            text=bui.Lstr(resource=f'{self._r}.titleText'),
            color=app.ui_v1.title_color,
            scale=titlescale,
            maxwidth=self._width - 340,
            h_align='center',
            v_align='center',
        )

        self._subcontainer: bui.Widget | None = None
        self._refresh()

    @override
    def get_main_window_state(self) -> bui.MainWindowState:
        # Support recreating our window for back/refresh purposes.
        cls = type(self)
        return bui.BasicMainWindowState(
            create_call=lambda transition, origin_widget: cls(
                transition=transition, origin_widget=origin_widget
            )
        )

    @override
    def main_window_should_preserve_selection(self) -> bool:
        return True

    def _update(self) -> None:
        plus = bui.app.plus
        assert plus is not None

        # If they want us to close once we're signed in, do so.
        if self._close_once_signed_in and self._v1_signed_in:
            self.main_window_back()
            return

        # Hmm should update this to use get_account_state_num.
        # Theoretically if we switch from one signed-in account to
        # another in the background this would break.
        v1_account_state_num = plus.get_v1_account_state_num()
        v1_account_state = plus.get_v1_account_state()

        if (
            v1_account_state_num != self._v1_account_state_num
            or self._needs_refresh
        ):
            self._v1_account_state_num = v1_account_state_num
            self._v1_signed_in = v1_account_state == 'signed_in'
            self._refresh()

        # Go ahead and refresh some individual things that may change
        # under us.
        self._refresh_campaign_progress_text()
        self._refresh_achievements()
        self._refresh_tickets_text()
        self._refresh_account_name_text()

    def _refresh(self) -> None:
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-branches
        # pylint: disable=too-many-locals
        # pylint: disable=cyclic-import

        plus = bui.app.plus
        assert plus is not None

        via_lines: list[str] = []

        primary_v2_account = plus.accounts.primary

        v1_state = plus.get_v1_account_state()
        v1_account_type = (
            plus.get_v1_account_type() if v1_state == 'signed_in' else 'unknown'
        )

        # We expose GPGS-specific functionality only if it is 'active'
        # (meaning the current GPGS player matches one of our account's
        # logins).
        adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
        gpgs_active = adapter is not None and adapter.is_back_end_active()

        # Ditto for Game Center.
        adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
        game_center_active = (
            adapter is not None and adapter.is_back_end_active()
        )

        show_signed_in_as = self._v1_signed_in
        signed_in_as_space = 95.0

        # To reduce confusion about the whole V2 account situation for
        # people used to seeing their Google Play Games or Game Center
        # account name and icon and whatnot, let's show those underneath
        # the V2 tag to help communicate that they are in fact logged in
        # through that account.
        via_space = 25.0
        if show_signed_in_as and bui.app.plus is not None:
            accounts = bui.app.plus.accounts
            if accounts.primary is not None:
                # GPGS / Game Center: show 'via' only if the adapter is
                # currently back-end-active (don't show 'via Game Center'
                # when running on a platform where Game Center is
                # irrelevant, even if the attachment exists server-side).
                for ltype, lchar in [
                    (LoginType.GPGS, bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
                    (LoginType.GAME_CENTER, bui.SpecialChar.GAME_CENTER_LOGO),
                ]:
                    linfo = accounts.primary.logins.get(ltype)
                    ladapter = accounts.login_adapters.get(ltype)
                    if (
                        linfo is not None
                        and ladapter is not None
                        and ladapter.is_back_end_active()
                    ):
                        via_lines.append(f'{bui.charstr(lchar)}{linfo.name}')

                # Discord is a global service (not platform-scoped), so
                # server attachment is the source of truth; no SDK-
                # connection gating needed.
                linfo = accounts.primary.logins.get(LoginType.DISCORD)
                if linfo is not None:
                    via_lines.append(
                        f'{bui.charstr(bui.SpecialChar.DISCORD_LOGO)}'
                        f'{linfo.name}'
                    )

                # TEMP TESTING
                if bool(False):
                    icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
                    via_lines.append(f'{icontxt}FloofDibble')
                    icontxt = bui.charstr(
                        bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO
                    )
                    via_lines.append(f'{icontxt}StinkBobble')

        show_sign_in_benefits = not self._v1_signed_in
        sign_in_benefits_space = 80.0

        sign_in_in_progress = (
            self._signing_in_adapter is not None or self._signing_in_discord
        )

        show_signing_in_text = v1_state == 'signing_in' or sign_in_in_progress
        signing_in_text_space = 80.0

        show_google_play_sign_in_button = (
            v1_state == 'signed_out'
            and not sign_in_in_progress
            and 'Google Play' in self._show_sign_in_buttons
        )
        show_game_center_sign_in_button = (
            v1_state == 'signed_out'
            and not sign_in_in_progress
            and 'Game Center' in self._show_sign_in_buttons
        )
        show_discord_sign_in_button = (
            v1_state == 'signed_out'
            and not sign_in_in_progress
            and 'Discord' in self._show_sign_in_buttons
        )
        show_v2_proxy_sign_in_button = (
            v1_state == 'signed_out'
            and not sign_in_in_progress
            and 'V2Proxy' in self._show_sign_in_buttons
        )
        show_device_sign_in_button = (
            v1_state == 'signed_out'
            and not sign_in_in_progress
            and 'Device' in self._show_sign_in_buttons
        )
        sign_in_button_space = 70.0
        deprecated_space = 60

        # Game Center currently has a single UI for everything.
        show_game_center_button = game_center_active
        game_center_button_space = 60.0

        # Phasing this out (for V2 accounts at least).
        show_linked_accounts_text = (
            self._v1_signed_in and v1_account_type != 'V2'
        )
        linked_accounts_text_space = 60.0

        # Update: No longer showing this since its visible on main
        # toolbar.
        show_achievements_text = False
        achievements_text_space = 27.0

        show_leaderboards_button = self._v1_signed_in and gpgs_active
        leaderboards_button_space = 60.0

        # Update: No longer showing this; trying to get progress type
        # stuff out of the account panel.
        # show_campaign_progress = self._v1_signed_in
        show_campaign_progress = False
        campaign_progress_space = 27.0

        # show_tickets = self._v1_signed_in
        show_tickets = False
        tickets_space = 27.0

        show_manage_account_button = primary_v2_account is not None
        manage_account_button_space = 70.0

        show_create_account_button = show_v2_proxy_sign_in_button
        create_account_button_space = 70.0

        # Apple asks us to make a delete-account button directly
        # available in the UI. Currently disabling this elsewhere
        # however as I feel that poking 'Manage Account' and scrolling
        # down to 'Delete Account' is not hard to find.
        show_delete_account_button = primary_v2_account is not None and (
            bui.app.classic is not None
            and bui.app.classic.platform == 'mac'
            and bui.app.classic.subplatform == 'appstore'
        )
        delete_account_button_space = 70.0

        # show_link_accounts_button = self._v1_signed_in and (
        #     primary_v2_account is None or FORCE_ENABLE_V1_LINKING
        # )
        # link_accounts_button_space = 70.0

        show_v1_obsolete_note = self._v1_signed_in and (
            primary_v2_account is None
        )
        v1_obsolete_note_space = 80.0

        show_sign_out_button = primary_v2_account is not None or (
            self._v1_signed_in and v1_account_type == 'Local'
        )
        sign_out_button_space = 70.0

        # We can show cancel if we're either waiting on a sign-in flow
        # to provide us with v2 credentials or waiting for those
        # credentials to be verified.
        show_cancel_sign_in_button = sign_in_in_progress or (
            plus.accounts.have_primary_credentials()
            and primary_v2_account is None
        )
        cancel_sign_in_button_space = 70.0

        if self._subcontainer is not None:
            self._subcontainer.delete()
        self._sub_height = 90.0

        # For fullscreen scrollable, account for toolbar.
        uiscale = bui.app.ui_v1.uiscale
        if uiscale is bui.UIScale.SMALL:
            self._sub_height += 35

        if show_signed_in_as:
            self._sub_height += signed_in_as_space
        self._sub_height += via_space * len(via_lines)
        if show_signing_in_text:
            self._sub_height += signing_in_text_space
        if show_google_play_sign_in_button:
            self._sub_height += sign_in_button_space
        if show_game_center_sign_in_button:
            self._sub_height += sign_in_button_space
        if show_discord_sign_in_button:
            self._sub_height += sign_in_button_space
        if show_v2_proxy_sign_in_button:
            self._sub_height += sign_in_button_space
        if show_device_sign_in_button:
            self._sub_height += sign_in_button_space + deprecated_space
        if show_game_center_button:
            self._sub_height += game_center_button_space
        if show_linked_accounts_text:
            self._sub_height += linked_accounts_text_space
        if show_achievements_text:
            self._sub_height += achievements_text_space
        if show_leaderboards_button:
            self._sub_height += leaderboards_button_space
        if show_campaign_progress:
            self._sub_height += campaign_progress_space
        if show_tickets:
            self._sub_height += tickets_space
        if show_sign_in_benefits:
            self._sub_height += sign_in_benefits_space
        if show_manage_account_button:
            self._sub_height += manage_account_button_space
        if show_create_account_button:
            self._sub_height += create_account_button_space
        if show_v1_obsolete_note:
            self._sub_height += v1_obsolete_note_space
        if show_sign_out_button:
            self._sub_height += sign_out_button_space
        if show_delete_account_button:
            self._sub_height += delete_account_button_space
        if show_cancel_sign_in_button:
            self._sub_height += cancel_sign_in_button_space
        self._subcontainer = bui.containerwidget(
            parent=self._scrollwidget,
            size=(self._sub_width, self._sub_height),
            background=False,
            claims_left_right=True,
            selection_loops_to_parent=True,
        )

        first_selectable = None
        v = self._sub_height - 10.0

        # For fullscreen scrollable, account for toolbar.
        if uiscale is bui.UIScale.SMALL:
            v -= 35

        assert bui.app.classic is not None
        self._account_name_text: bui.Widget | None
        if show_signed_in_as:
            v -= signed_in_as_space * 0.2
            txt = bui.Lstr(
                resource='accountSettingsWindow.youAreSignedInAsText',
                fallback_resource='accountSettingsWindow.youAreLoggedInAsText',
            )
            bui.textwidget(
                parent=self._subcontainer,
                position=(self._sub_width * 0.5, v),
                size=(0, 0),
                text=txt,
                scale=0.9,
                color=bui.app.ui_v1.title_color,
                maxwidth=self._sub_width * 0.9,
                h_align='center',
                v_align='center',
            )
            v -= signed_in_as_space * 0.5
            self._account_name_text = bui.textwidget(
                parent=self._subcontainer,
                position=(self._sub_width * 0.5, v),
                size=(0, 0),
                scale=1.5,
                maxwidth=self._sub_width * 0.9,
                res_scale=1.5,
                color=(1, 1, 1, 1),
                h_align='center',
                v_align='center',
            )

            self._refresh_account_name_text()

            v -= signed_in_as_space * 0.4

            for via in via_lines:
                v -= via_space * 0.1
                sscale = 0.7
                swidth = (
                    bui.get_string_width(via, suppress_warning=True) * sscale
                )
                bui.textwidget(
                    parent=self._subcontainer,
                    position=(self._sub_width * 0.5, v),
                    size=(0, 0),
                    text=via,
                    scale=sscale,
                    color=(0.6, 0.6, 0.6),
                    flatness=1.0,
                    shadow=0.0,
                    h_align='center',
                    v_align='center',
                )
                bui.textwidget(
                    parent=self._subcontainer,
                    position=(self._sub_width * 0.5 - swidth * 0.5 - 5, v),
                    size=(0, 0),
                    text=bui.Lstr(
                        value='(${VIA}',
                        subs=[('${VIA}', bui.Lstr(resource='viaText'))],
                    ),
                    scale=0.5,
                    color=(0.4, 0.6, 0.4, 0.5),
                    flatness=1.0,
                    shadow=0.0,
                    h_align='right',
                    v_align='center',
                )
                bui.textwidget(
                    parent=self._subcontainer,
                    position=(self._sub_width * 0.5 + swidth * 0.5 + 10, v),
                    size=(0, 0),
                    text=')',
                    scale=0.5,
                    color=(0.4, 0.6, 0.4, 0.5),
                    flatness=1.0,
                    shadow=0.0,
                    h_align='right',
                    v_align='center',
                )

                v -= via_space * 0.9

        else:
            self._account_name_text = None

        if self._back_button is None:
            bbtn = bui.get_special_widget('back_button')
        else:
            bbtn = self._back_button

        if show_sign_in_benefits:
            v -= sign_in_benefits_space
            bui.textwidget(
                parent=self._subcontainer,
                position=(
                    self._sub_width * 0.5,
                    v + sign_in_benefits_space * 0.4,
                ),
                size=(0, 0),
                text=bui.Lstr(resource=f'{self._r}.signInInfoText'),
                max_height=sign_in_benefits_space * 0.9,
                scale=0.9,
                color=(0.75, 0.7, 0.8),
                maxwidth=self._sub_width * 0.8,
                h_align='center',
                v_align='center',
            )

        if show_signing_in_text:
            v -= signing_in_text_space

            bui.textwidget(
                parent=self._subcontainer,
                position=(
                    self._sub_width * 0.5,
                    v + signing_in_text_space * 0.5,
                ),
                size=(0, 0),
                text=bui.Lstr(resource='accountSettingsWindow.signingInText'),
                scale=0.9,
                color=(0, 1, 0),
                maxwidth=self._sub_width * 0.8,
                h_align='center',
                v_align='center',
            )

        if show_google_play_sign_in_button:
            button_width = 350
            v -= sign_in_button_space
            btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|signingoogleplay',
                position=((self._sub_width - button_width) * 0.5, v - 20),
                autoselect=True,
                size=(button_width, 60),
                label=bui.Lstr(
                    value='${A} ${B}',
                    subs=[
                        (
                            '${A}',
                            bui.charstr(bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
                        ),
                        (
                            '${B}',
                            bui.Lstr(
                                resource=f'{self._r}.signInWithText',
                                subs=[
                                    (
                                        '${SERVICE}',
                                        bui.Lstr(resource='googlePlayText'),
                                    )
                                ],
                            ),
                        ),
                    ],
                ),
                on_activate_call=lambda: self._sign_in_press(LoginType.GPGS),
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)
            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
            self._sign_in_text = None

        if show_game_center_sign_in_button:
            button_width = 350
            v -= sign_in_button_space
            btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|signingamecenter',
                position=((self._sub_width - button_width) * 0.5, v - 20),
                autoselect=True,
                size=(button_width, 60),
                # Note: Apparently Game Center is just called 'Game Center'
                # in all languages. Can revisit if not true.
                # https://developer.apple.com/forums/thread/725779
                label=bui.Lstr(
                    value='${A} ${B}',
                    subs=[
                        (
                            '${A}',
                            bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO),
                        ),
                        (
                            '${B}',
                            bui.Lstr(
                                resource=f'{self._r}.signInWithText',
                                subs=[('${SERVICE}', 'Game Center')],
                            ),
                        ),
                    ],
                ),
                on_activate_call=lambda: self._sign_in_press(
                    LoginType.GAME_CENTER
                ),
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)
            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
            self._sign_in_text = None

        if show_discord_sign_in_button:
            button_width = 350
            v -= sign_in_button_space
            btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|signindiscord',
                position=((self._sub_width - button_width) * 0.5, v - 20),
                autoselect=True,
                size=(button_width, 60),
                # "Discord" is a brand name so we pass it as a literal
                # (same pattern Game Center uses); the surrounding
                # "Sign in with..." comes from a translated resource.
                label=bui.Lstr(
                    value='${A} ${B}',
                    subs=[
                        (
                            '${A}',
                            bui.charstr(bui.SpecialChar.DISCORD_LOGO),
                        ),
                        (
                            '${B}',
                            bui.Lstr(
                                resource=f'{self._r}.signInWithText',
                                subs=[('${SERVICE}', 'Discord')],
                            ),
                        ),
                    ],
                ),
                on_activate_call=self._discord_sign_in_press,
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)
            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
            self._sign_in_text = None

        if show_v2_proxy_sign_in_button:
            button_width = 350
            v -= sign_in_button_space
            self._sign_in_v2_proxy_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|signinv2',
                position=((self._sub_width - button_width) * 0.5, v - 20),
                autoselect=True,
                size=(button_width, 60),
                label='',
                on_activate_call=self._v2_proxy_sign_in_press,
            )

            v2labeltext: bui.Lstr | str = (
                bui.Lstr(resource=f'{self._r}.signInWithAnEmailAddressText')
                if show_game_center_sign_in_button
                or show_google_play_sign_in_button
                or show_discord_sign_in_button
                or show_device_sign_in_button
                else bui.Lstr(resource=f'{self._r}.signInText')
            )
            v2infotext: bui.Lstr | str | None = None

            bui.textwidget(
                parent=self._subcontainer,
                draw_controller=btn,
                h_align='center',
                v_align='center',
                size=(0, 0),
                position=(
                    self._sub_width * 0.5,
                    v + (17 if v2infotext is not None else 10),
                ),
                text=bui.Lstr(
                    value='${A} ${B}',
                    subs=[
                        ('${A}', bui.charstr(bui.SpecialChar.V2_LOGO)),
                        (
                            '${B}',
                            v2labeltext,
                        ),
                    ],
                ),
                maxwidth=button_width * 0.8,
                color=(0.75, 1.0, 0.7),
            )
            if v2infotext is not None:
                bui.textwidget(
                    parent=self._subcontainer,
                    draw_controller=btn,
                    h_align='center',
                    v_align='center',
                    size=(0, 0),
                    position=(self._sub_width * 0.5, v - 4),
                    text=v2infotext,
                    flatness=1.0,
                    scale=0.57,
                    maxwidth=button_width * 0.9,
                    color=(0.55, 0.8, 0.5),
                )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)
            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
            self._sign_in_text = None

        if show_device_sign_in_button:
            button_width = 350
            v -= sign_in_button_space + deprecated_space
            self._sign_in_device_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|signindevice',
                position=((self._sub_width - button_width) * 0.5, v - 20),
                autoselect=True,
                size=(button_width, 60),
                label='',
                on_activate_call=lambda: self._sign_in_press('Local'),
            )
            bui.textwidget(
                parent=self._subcontainer,
                h_align='center',
                v_align='center',
                size=(0, 0),
                position=(self._sub_width * 0.5, v + 60),
                text=bui.Lstr(resource='deprecatedText'),
                scale=0.8,
                maxwidth=300,
                color=(0.6, 0.55, 0.45),
            )

            bui.textwidget(
                parent=self._subcontainer,
                draw_controller=btn,
                h_align='center',
                v_align='center',
                size=(0, 0),
                position=(self._sub_width * 0.5, v + 17),
                text=bui.Lstr(
                    value='${A} ${B}',
                    subs=[
                        ('${A}', bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT)),
                        (
                            '${B}',
                            bui.Lstr(
                                resource=f'{self._r}.signInWithDeviceText'
                            ),
                        ),
                    ],
                ),
                maxwidth=button_width * 0.8,
                color=(0.75, 1.0, 0.7),
            )
            bui.textwidget(
                parent=self._subcontainer,
                draw_controller=btn,
                h_align='center',
                v_align='center',
                size=(0, 0),
                position=(self._sub_width * 0.5, v - 4),
                text=bui.Lstr(resource=f'{self._r}.signInWithDeviceInfoText'),
                flatness=1.0,
                scale=0.57,
                maxwidth=button_width * 0.9,
                color=(0.55, 0.8, 0.5),
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)
            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
            self._sign_in_text = None

        if show_v1_obsolete_note:
            v -= v1_obsolete_note_space
            bui.textwidget(
                parent=self._subcontainer,
                h_align='center',
                v_align='center',
                size=(0, 0),
                position=(self._sub_width * 0.5, v + 35.0),
                text=(
                    'YOU ARE SIGNED IN WITH A V1 ACCOUNT.\n'
                    'THESE ARE NO LONGER SUPPORTED AND MANY\n'
                    'FEATURES WILL NOT WORK. PLEASE SWITCH TO\n'
                    'A V2 ACCOUNT OR UPGRADE THIS ONE.'
                ),
                maxwidth=self._sub_width * 0.8,
                color=(1, 0, 0),
                shadow=1.0,
                flatness=1.0,
            )

        if show_manage_account_button:
            button_width = 300
            v -= manage_account_button_space
            self._manage_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|manage',
                position=((self._sub_width - button_width) * 0.5, v),
                autoselect=True,
                size=(button_width, 60),
                label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
                color=(0.55, 0.5, 0.6),
                icon=stdassets.textures.settings_icon,
                textcolor=(0.75, 0.7, 0.8),
                on_activate_call=bui.WeakCallStrict(
                    self._on_manage_account_press
                ),
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)

        if show_create_account_button:
            button_width = 300
            v -= create_account_button_space
            self._create_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|create',
                position=((self._sub_width - button_width) * 0.5, v - 30),
                autoselect=True,
                size=(button_width, 60),
                label=bui.Lstr(resource=f'{self._r}.createAnAccountText'),
                color=(0.55, 0.5, 0.6),
                textcolor=(0.75, 0.7, 0.8),
                on_activate_call=bui.WeakCallStrict(
                    self._on_create_account_press
                ),
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)

        # the button to go to OS-Specific leaderboards/high-score-lists/etc.
        if show_game_center_button:
            button_width = 300
            v -= game_center_button_space * 1.0
            if game_center_active:
                # Note: Apparently Game Center is just called 'Game Center'
                # in all languages. Can revisit if not true.
                # https://developer.apple.com/forums/thread/725779
                game_center_button_label = bui.Lstr(
                    value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
                    + 'Game Center'
                )
            else:
                raise ValueError(
                    "unknown account type: '" + str(v1_account_type) + "'"
                )
            self._game_center_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|gamecenter',
                position=((self._sub_width - button_width) * 0.5, v),
                color=(0.55, 0.5, 0.6),
                textcolor=(0.75, 0.7, 0.8),
                autoselect=True,
                on_activate_call=self._on_game_center_button_press,
                size=(button_width, 50),
                label=game_center_button_label,
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)
            v -= game_center_button_space * 0.4
        else:
            self.game_center_button = None

        self._achievements_text: bui.Widget | None
        if show_achievements_text:
            v -= achievements_text_space * 0.5
            self._achievements_text = bui.textwidget(
                parent=self._subcontainer,
                position=(self._sub_width * 0.5, v),
                size=(0, 0),
                scale=0.9,
                color=(0.75, 0.7, 0.8),
                maxwidth=self._sub_width * 0.8,
                h_align='center',
                v_align='center',
            )
            v -= achievements_text_space * 0.5
        else:
            self._achievements_text = None

        if show_achievements_text:
            self._refresh_achievements()

        self._leaderboards_button: bui.Widget | None
        if show_leaderboards_button:
            button_width = 300
            v -= leaderboards_button_space
            self._leaderboards_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|leaderboards',
                position=((self._sub_width - button_width) * 0.5, v),
                color=(0.55, 0.5, 0.6),
                textcolor=(0.75, 0.7, 0.8),
                autoselect=True,
                icon=stdassets.textures.google_play_leaderboards_icon,
                icon_color=(0.8, 0.95, 0.7),
                on_activate_call=self._on_leaderboards_press,
                size=(button_width, 50),
                label=bui.Lstr(resource='leaderboardsText'),
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn)
        else:
            self._leaderboards_button = None

        self._campaign_progress_text: bui.Widget | None
        if show_campaign_progress:
            v -= campaign_progress_space * 0.5
            self._campaign_progress_text = bui.textwidget(
                parent=self._subcontainer,
                position=(self._sub_width * 0.5, v),
                size=(0, 0),
                scale=0.9,
                color=(0.75, 0.7, 0.8),
                maxwidth=self._sub_width * 0.8,
                h_align='center',
                v_align='center',
            )
            v -= campaign_progress_space * 0.5
            self._refresh_campaign_progress_text()
        else:
            self._campaign_progress_text = None

        self._tickets_text: bui.Widget | None
        if show_tickets:
            v -= tickets_space * 0.5
            self._tickets_text = bui.textwidget(
                parent=self._subcontainer,
                position=(self._sub_width * 0.5, v),
                size=(0, 0),
                scale=0.9,
                color=(0.75, 0.7, 0.8),
                maxwidth=self._sub_width * 0.8,
                flatness=1.0,
                h_align='center',
                v_align='center',
            )
            v -= tickets_space * 0.5
            self._refresh_tickets_text()

        else:
            self._tickets_text = None

        button_width = 300

        if show_sign_out_button:
            v -= sign_out_button_space
            self._sign_out_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|signout',
                position=((self._sub_width - button_width) * 0.5, v),
                size=(button_width, 60),
                label=bui.Lstr(resource=f'{self._r}.signOutText'),
                color=(0.55, 0.5, 0.6),
                textcolor=(0.75, 0.7, 0.8),
                autoselect=True,
                on_activate_call=self._sign_out_press,
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)

        if show_cancel_sign_in_button:
            v -= cancel_sign_in_button_space
            self._cancel_sign_in_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|cancelsignin',
                position=((self._sub_width - button_width) * 0.5, v),
                size=(button_width, 60),
                label=bui.Lstr(resource='cancelText'),
                color=(0.55, 0.5, 0.6),
                textcolor=(0.75, 0.7, 0.8),
                autoselect=True,
                on_activate_call=self._cancel_sign_in_press,
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)

        if show_delete_account_button:
            v -= delete_account_button_space
            self._delete_account_button = btn = bui.buttonwidget(
                parent=self._subcontainer,
                id=f'{self.main_window_id_prefix}|deleteaccount',
                position=((self._sub_width - button_width) * 0.5, v),
                size=(button_width, 60),
                label=bui.Lstr(resource=f'{self._r}.deleteAccountText'),
                color=(0.85, 0.5, 0.6),
                textcolor=(0.9, 0.7, 0.8),
                autoselect=True,
                on_activate_call=self._on_delete_account_press,
            )
            if first_selectable is None:
                first_selectable = btn
            bui.widget(
                edit=btn, right_widget=bui.get_special_widget('squad_button')
            )
            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)

        # Whatever the topmost selectable thing is, we want it to scroll all
        # the way up when we select it.
        if first_selectable is not None:
            bui.widget(
                edit=first_selectable, up_widget=bbtn, show_buffer_top=400
            )
            # (this should re-scroll us to the top..)
            bui.containerwidget(
                edit=self._subcontainer, visible_child=first_selectable
            )
        self._needs_refresh = False

    def _on_game_center_button_press(self) -> None:
        if bui.app.plus is not None:
            bui.app.plus.show_game_service_ui()
        else:
            logging.warning(
                'game-service-ui not available without plus feature-set.'
            )

    def _on_custom_achievements_press(self) -> None:
        if bui.app.plus is not None:
            bui.apptimer(
                0.15,
                bui.CallStrict(
                    bui.app.plus.show_game_service_ui, 'achievements'
                ),
            )
        else:
            logging.warning('show_game_service_ui requires plus feature-set.')

    def _on_manage_account_press(self) -> None:
        self._do_manage_account_press(WebLocation.ACCOUNT_EDITOR)

    def _on_create_account_press(self) -> None:
        bui.open_url('https://ballistica.net/createaccount')

    def _on_delete_account_press(self) -> None:
        self._do_manage_account_press(WebLocation.ACCOUNT_DELETE_SECTION)

    def _do_manage_account_press(self, weblocation: WebLocation) -> None:
        # If we're still waiting for our master-server connection,
        # keep the user informed of this instead of rushing in and
        # failing immediately.
        wait_for_connectivity(
            on_connected=lambda: self._do_manage_account(weblocation)
        )

    def _do_manage_account(self, weblocation: WebLocation) -> None:
        plus = bui.app.plus
        assert plus is not None

        bui.screenmessage(bui.Lstr(resource='oneMomentText'))

        # We expect to have a v2 account signed in if we get here.
        if plus.accounts.primary is None:
            logging.exception(
                'got manage-account press without v2 account present'
            )
            return

        with plus.accounts.primary:
            plus.cloud.send_message_cb(
                bacommon.cloud.ManageAccountMessage(weblocation=weblocation),
                on_response=bui.WeakCallPartial(
                    self._on_manage_account_response
                ),
            )

    def _on_manage_account_response(
        self, response: bacommon.cloud.ManageAccountResponse | Exception
    ) -> None:
        if isinstance(response, Exception) or response.url is None:
            logging.warning(
                'Got error in manage-account-response: %s.', response
            )
            bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
            builtinassets.audio.error.play()
            return

        bui.open_url(response.url)

    def _on_leaderboards_press(self) -> None:
        if bui.app.plus is not None:
            bui.apptimer(
                0.15,
                bui.CallStrict(
                    bui.app.plus.show_game_service_ui, 'leaderboards'
                ),
            )
        else:
            logging.warning('show_game_service_ui requires classic')

    def _refresh_campaign_progress_text(self) -> None:
        if self._campaign_progress_text is None:
            return
        p_str: str | bui.Lstr
        try:
            assert bui.app.classic is not None
            campaign = bui.app.classic.getcampaign('Default')
            levels = campaign.levels
            levels_complete = sum((1 if l.complete else 0) for l in levels)

            # Last level cant be completed; hence the -1;
            progress = min(1.0, float(levels_complete) / (len(levels) - 1))
            p_str = bui.Lstr(
                resource=f'{self._r}.campaignProgressText',
                subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')],
            )
        except Exception:
            p_str = '?'
            logging.exception('Error calculating co-op campaign progress.')
        bui.textwidget(edit=self._campaign_progress_text, text=p_str)

    def _refresh_tickets_text(self) -> None:
        classic = bui.app.classic
        assert classic is not None

        if self._tickets_text is None:
            return
        try:
            tc_str = str(classic.tickets)
        except Exception:
            logging.exception('error refreshing tickets text')
            tc_str = '-'
        bui.textwidget(
            edit=self._tickets_text,
            text=bui.Lstr(
                resource=f'{self._r}.ticketsText', subs=[('${COUNT}', tc_str)]
            ),
        )

    def _refresh_account_name_text(self) -> None:
        plus = bui.app.plus
        assert plus is not None

        if self._account_name_text is None:
            return
        try:
            name_str = plus.get_v1_account_display_string()
        except Exception:
            logging.exception('error refreshing tickets text')
            name_str = '??'

        bui.textwidget(edit=self._account_name_text, text=name_str)

    def _refresh_achievements(self) -> None:
        assert bui.app.classic is not None
        if self._achievements_text is None:
            return
        complete = sum(
            1 if a.complete else 0 for a in bui.app.classic.ach.achievements
        )
        total = len(bui.app.classic.ach.achievements)
        txt_final = bui.Lstr(
            resource=f'{self._r}.achievementProgressText',
            subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))],
        )

        if self._achievements_text is not None:
            bui.textwidget(edit=self._achievements_text, text=txt_final)

    def _cancel_sign_in_press(self) -> None:
        # If we're waiting on an adapter or Discord flow to give us
        # credentials, abort.
        self._signing_in_adapter = None
        self._signing_in_discord = False

        plus = bui.app.plus
        assert plus is not None

        # Say we don't wanna be signed in anymore if we are.
        plus.accounts.set_primary_credentials(None)

        self._needs_refresh = True

        # Speed UI updates along.
        bui.apptimer(0.1, bui.WeakCallStrict(self._update))

    def _sign_out_press(self) -> None:
        plus = bui.app.plus
        assert plus is not None

        if plus.accounts.have_primary_credentials():
            if (
                plus.accounts.primary is not None
                and LoginType.GPGS in plus.accounts.primary.logins
            ):
                self._explicitly_signed_out_of_gpgs = True
            plus.accounts.set_primary_credentials(None)
        else:
            plus.sign_out_v1()

        cfg = bui.app.config

        # Also take note that its our *explicit* intention to not be
        # signed in at this point (affects v1 accounts).
        cfg['Auto Account State'] = 'signed_out'
        cfg.commit()
        bui.buttonwidget(
            edit=self._sign_out_button,
            label=bui.Lstr(resource=f'{self._r}.signingOutText'),
        )

        # Speed UI updates along.
        bui.apptimer(0.1, bui.WeakCallStrict(self._update))

    def _sign_in_press(self, login_type: str | LoginType) -> None:

        # Any time we initiate a sign in, turn off auto-recreates for
        # the remainder of our existence. We want to make sure we stick
        # around to handle the entire sign-in flow, and things like
        # system sign-in dialogs could potentially cause toolbars to
        # show/hide or other things that could affect window dimensions
        # and thus trigger window recreates which could interrupt our
        # process.
        self._recreate_suppress = bui.MainWindowAutoRecreateSuppress()

        # If we're still waiting for our master-server connection, keep
        # the user informed of this instead of rushing in and failing
        # immediately.
        wait_for_connectivity(on_connected=lambda: self._sign_in(login_type))

    def _sign_in(self, login_type: str | LoginType) -> None:
        plus = bui.app.plus
        assert plus is not None

        # V1 login types are strings.
        if isinstance(login_type, str):
            plus.sign_in_v1(login_type)

            # Make note of the type account we're *wanting*
            # to be signed in with.
            cfg = bui.app.config
            cfg['Auto Account State'] = login_type
            cfg.commit()
            self._needs_refresh = True
            bui.apptimer(0.1, bui.WeakCallStrict(self._update))
            return

        # V2 login sign-in buttons generally go through adapters.
        adapter = plus.accounts.login_adapters.get(login_type)
        if adapter is not None:
            self._signing_in_adapter = adapter
            adapter.sign_in(
                result_cb=bui.WeakCallPartial(self._on_adapter_sign_in_result),
                description='account settings button',
            )
            # Will get 'Signing in...' to show.
            self._needs_refresh = True
            bui.apptimer(0.1, bui.WeakCallStrict(self._update))
        else:
            bui.screenmessage(f'Unsupported login_type: {login_type.name}')

    def _on_adapter_sign_in_result(
        self,
        adapter: bui.LoginAdapter,
        result: bui.LoginAdapter.SignInResult | Exception,
    ) -> None:
        is_us = self._signing_in_adapter is adapter

        # If this isn't our current one we don't care.
        if not is_us:
            return

        # If it is us, note that we're done.
        self._signing_in_adapter = None

        if isinstance(result, Exception):
            # For now just make a bit of noise if anything went wrong;
            # can get more specific as needed later.
            logging.warning('Got error in v2 sign-in result: %s', result)
            bui.screenmessage(
                bui.Lstr(resource='internal.signInNoConnectionText'),
                color=(1, 0, 0),
            )
            builtinassets.audio.error.play()
        else:
            # Success! Plug in these credentials which will begin
            # verifying them and set our primary account-handle when
            # finished.
            plus = bui.app.plus
            assert plus is not None
            plus.accounts.set_primary_credentials(result.credentials)

            # Special case - if the user has explicitly signed out and
            # signed in again with GPGS via this button, warn them that
            # they need to use the app if they want to switch to a
            # different GPGS account.
            if (
                self._explicitly_signed_out_of_gpgs
                and adapter.login_type is LoginType.GPGS
            ):
                # Delay this slightly so it hopefully pops up after
                # credentials go through and the account name shows up.
                bui.apptimer(
                    1.5,
                    bui.CallStrict(
                        bui.screenmessage,
                        bui.Lstr(
                            resource=self._r
                            + '.googlePlayGamesAccountSwitchText'
                        ),
                    ),
                )

        # Speed any UI updates along.
        self._needs_refresh = True
        bui.apptimer(0.1, bui.WeakCallStrict(self._update))

    def _discord_sign_in_press(self) -> None:

        # Any time we initiate a sign in, turn off auto-recreates for
        # the remainder of our existence — see _sign_in_press.
        self._recreate_suppress = bui.MainWindowAutoRecreateSuppress()

        wait_for_connectivity(on_connected=self._discord_sign_in)

    def _discord_sign_in(self) -> None:
        self._signing_in_discord = True
        bui.discord_sign_in(
            result_cb=bui.WeakCallPartial(self._on_discord_sign_in_result),
            description='account settings button',
        )
        # Will get 'Signing in...' to show.
        self._needs_refresh = True
        bui.apptimer(0.1, bui.WeakCallStrict(self._update))

    def _on_discord_sign_in_result(self, result: str | Exception) -> None:
        # If the user cancelled mid-flow, discard.
        if not self._signing_in_discord:
            return
        self._signing_in_discord = False

        if isinstance(result, Exception):
            logging.warning('Got error in discord sign-in result: %s', result)
            bui.screenmessage(
                bui.Lstr(resource='internal.signInNoConnectionText'),
                color=(1, 0, 0),
            )
            builtinassets.audio.error.play()
        else:
            plus = bui.app.plus
            assert plus is not None
            plus.accounts.set_primary_credentials(result)

        self._needs_refresh = True
        bui.apptimer(0.1, bui.WeakCallStrict(self._update))

    def _v2_proxy_sign_in_press(self) -> None:

        # Any time we initiate a sign in, turn off auto-recreates for
        # the remainder of our existence. We want to make sure we stick
        # around to handle the entire sign-in flow, and things like
        # system sign-in dialogs could potentially cause toolbars to
        # show/hide or other things that could affect window dimensions
        # and thus trigger window recreates which could interrupt our
        # process.
        self._recreate_suppress = bui.MainWindowAutoRecreateSuppress()

        # If we're still waiting for our master-server connection, keep
        # the user informed of this instead of rushing in and failing
        # immediately.
        wait_for_connectivity(on_connected=self._v2_proxy_sign_in)

    def _v2_proxy_sign_in(self) -> None:
        # pylint: disable=cyclic-import
        from bauiv1lib.account.v2proxy import V2ProxySignInWindow

        assert self._sign_in_v2_proxy_button is not None
        V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)


def show_what_is_legacy_unlinking_page() -> None:
    """Show the webpage describing legacy unlinking."""
    plus = bui.app.plus
    assert plus is not None

    bamasteraddr = plus.get_master_server_address()
    bui.open_url(f'{bamasteraddr}/whatarev1links')
