#!/usr/bin/env python

"""Remove data for user accounts that have been marked as deleted.

:Copyright: 2006-2020 Jochen Kupperschmidt
:License: Modified BSD, see LICENSE for details.
"""

from typing import Callable, Set

import click

from byceps.database import db
from byceps.services.authentication.password.models import Credential
from byceps.services.authentication.session.models.session_token import (
    SessionToken,
)
from byceps.services.authorization.models import UserRole
from byceps.services.board.models.last_category_view import (
    LastCategoryView as BoardLastCategoryView,
)
from byceps.services.board.models.last_topic_view import (
    LastTopicView as BoardLastTopicView,
)
from byceps.services.consent.models.consent import Consent
from byceps.services.newsletter.models import (
    SubscriptionUpdate as NewsletterSubscriptionUpdate,
)
from byceps.services.user.models.event import UserEvent
from byceps.services.user import service as user_service
from byceps.services.user_avatar.models import (
    AvatarSelection as UserAvatarSelection,
)
from byceps.services.verification_token.models import Token as VerificationToken
from byceps.typing import UserID
from byceps.util.system import get_config_filename_from_env_or_exit

from _util import app_context


@click.command()
@click.option(
    '--dry-run',
    is_flag=True,
    help='determine but do not delete affected records',
)
@click.argument('user_ids', nargs=-1, required=True)
def execute(dry_run, user_ids):
    user_ids = set(user_ids)

    check_for_undeleted_accounts(user_ids)

    def delete(label: str, delete_func: Callable) -> None:
        delete_records(label, delete_func, user_ids)

    delete('authentication credentials', delete_authn_credentials)
    delete('session tokens', delete_authn_session_tokens)
    delete('authorization role assignments', delete_authz_user_roles)
    delete('board category view marks', delete_board_category_lastviews)
    delete('board topic view marks', delete_board_topic_lastviews)
    delete('consents', delete_consents)
    delete(
        'newsletter subscription updates',
        delete_newsletter_subscription_updates,
    )
    delete('user avatar selections', delete_user_avatar_selections)
    delete('user events', delete_user_events)
    delete('verification tokens', delete_verification_tokens)

    if not dry_run:
        db.session.commit()


def check_for_undeleted_accounts(user_ids: Set[UserID]) -> None:
    users = user_service.find_users(user_ids)

    non_deleted_users = [u for u in users if not u.deleted]
    if non_deleted_users:
        user_ids_string = ', '.join(str(u.id) for u in non_deleted_users)
        raise click.BadParameter(
            f'These user accounts are not marked as deleted: {user_ids_string}'
        )


def delete_records(
    label: str, delete_func: Callable, user_ids: Set[UserID]
) -> None:
    click.secho(f'Deleting {label} ... ', nl=False)
    affected = delete_func(user_ids)
    click.secho(str(affected), fg='yellow')


def delete_authn_credentials(user_ids: Set[UserID]) -> int:
    """Delete authentication credentials for the given users."""
    return _execute_delete_for_users_query(Credential, user_ids)


def delete_authn_session_tokens(user_ids: Set[UserID]) -> int:
    """Delete session tokens for the given users."""
    return _execute_delete_for_users_query(SessionToken, user_ids)


def delete_authz_user_roles(user_ids: Set[UserID]) -> int:
    """Delete authorization role assignments from the given users."""
    return _execute_delete_for_users_query(UserRole, user_ids)


def delete_board_category_lastviews(user_ids: Set[UserID]) -> int:
    """Delete last board category view marks for the given users."""
    return _execute_delete_for_users_query(BoardLastCategoryView, user_ids)


def delete_board_topic_lastviews(user_ids: Set[UserID]) -> int:
    """Delete last board topic view marks for the given users."""
    return _execute_delete_for_users_query(BoardLastTopicView, user_ids)


def delete_consents(user_ids: Set[UserID]) -> int:
    """Delete consents from the given users."""
    return _execute_delete_for_users_query(Consent, user_ids)


def delete_newsletter_subscription_updates(user_ids: Set[UserID]) -> int:
    """Delete newsletter subscription updates for the given users."""
    return _execute_delete_for_users_query(
        NewsletterSubscriptionUpdate, user_ids
    )


def delete_user_avatar_selections(user_ids: Set[UserID]) -> int:
    """Delete user avatar selections (but not user avatar records and
    image files at this point) for the given users.
    """
    return _execute_delete_for_users_query(UserAvatarSelection, user_ids)


def delete_user_events(user_ids: Set[UserID]) -> int:
    """Delete user events (execpt for those that justify the deletion)
    for the given users.
    """
    return (
        db.session.query(UserEvent)
        .filter(UserEvent.user_id.in_(user_ids))
        .filter(UserEvent.event_type != 'user-deleted')
        .delete(synchronize_session=False)
    )


def delete_verification_tokens(user_ids: Set[UserID]) -> int:
    """Delete verification tokens for the given users."""
    return _execute_delete_for_users_query(VerificationToken, user_ids)


def _execute_delete_for_users_query(model, user_ids: Set[UserID]) -> int:
    """Execute (but not commit) deletions, return number of affected rows."""
    return (
        db.session.query(model)
        .filter(model.user_id.in_(user_ids))
        .delete(synchronize_session=False)
    )


if __name__ == '__main__':
    config_filename = get_config_filename_from_env_or_exit()
    with app_context(config_filename):
        execute()

Add script to remove (parts of) deleted users' data