# Generated by Django 3.2.8 on 2021-11-04 09:52

# Manual migration to replace Django OAuth Toolkit's models with custom models
# This migration is special, because it needs to work correctly both when applied
# during the initial database migration of the whole project, and when updating
# from an older schema version
#
# It strictly has to be applied before Django OAuth Toolkit's initial migration,
# because when the planner introspects existing models, it will stumble upon the swapped
# foreign key relations that now point to our custom models (as configured in settings).
#
# When run during the initial database migration, this simply works by setting run_before.
#
# When run during an update later, Django will refuse to migrate, because run_before will
# lead to an inconsistent migration history. Django OAuth Toolkit's migrations have already
# been run in the past, and we cannot move a new migration to before that.
#
# Therefore, we treat the two cases explicitly:
#  – If tables from the oauth2_provider label exist, we…
#     …assume that its migrations have been run in the past, and we are in an udpate
#     …forcefully drop the migration from the database to convince Django to let our own
#      migration run first
#     …rename the tables to the new app label through raw SQL, passing the original
#      operations that Django OAuth Toolkit would have done to the planner to spoof them
#      in the state
#  – If no tables from the oauth2_provider label exist, we…
#     …assume that we are in the first ever migration of the project
#     …introspect the operations that Django OAuth Toolkit would do, and re-create them for
#      our own app
#  – In both cases, we then let Django OAuth Toolkit do its migrations like normal; they will
#    will be no-ops as we swapped all models

from django.apps import apps as global_apps
from django.conf import settings
from django.db import connection, migrations
from django.utils.module_loading import import_string

MODEL_NAMES = ("Application", "Grant", "AccessToken", "IDToken", "RefreshToken")
_MIGRATION_NAMES = ["0001_initial", "0002_auto_20190406_1805", "0003_auto_20201211_1314", "0004_auto_20200902_2022"]


class Migration(migrations.Migration):
    """Empty dummy migration to satisfy loader in utility functions."""
    # This migration will later be replaced by the real one.
    # We only need a class called Migration here so the loader we use in
    # the functions below does not complain that this migration does not
    # have a Migration class (yet).
    pass

def _get_oauth_migrations():
    """Get all original migrations from Django OAuth Toolkit."""
    loader = migrations.loader.MigrationLoader(connection)
    return [loader.get_migration("oauth2_provider", name) for name in _MIGRATION_NAMES]

def _get_oauth_migration_operations(for_model=None):
    """Get all original operations from Django Oauth Toolkit and re-create them for the new models."""
    operations = []
    for migration in _get_oauth_migrations():
        for old_operation in migration.operations:
            # We need to re-create a new Operation class because Operations
            # are immutably linked to an app
            op_name, args, kwargs = old_operation.deconstruct()
            if not "." in op_name:
                op_name = f"django.db.migrations.{op_name}"
            op_cls = import_string(op_name)
            if "model_name" in kwargs:
                # If we are not interested for this model, skip it
                if for_model is not None and for_model != "oauth" + kwargs["model_name"].lower():
                    continue
                # We are modifying a field. Replace the model name it belongs to
                kwargs["model_name"] = "OAuth" + kwargs["model_name"]
            elif "name" in kwargs:
                # If we are not interested for this model, skip it
                if for_model is not None and for_model != "oauth" + kwargs["name"].lower():
                    continue
                # We are modifying a model. Replace the full name
                kwargs["name"] = "OAuth" + kwargs["name"]
            operations.append(op_cls(*args, **kwargs))

    return operations

def _get_original_models():
    """Get the original models of Django OAuth Toolkit."""
    try:
        return [global_apps.get_model("oauth2_provider", name) for name in MODEL_NAMES]
    except LookupError:
        return []

def _get_new_models():
    """Get the new customised models."""
    return [global_apps.get_model("core", f"OAuth{name}") for name in MODEL_NAMES]

def _original_tables_exist():
    """Check if original Django OAuth Toolkit tables exist."""
    original_tables = set([model._meta.db_table for model in _get_original_models()])
    return bool(set(connection.introspection.table_names()) & original_tables)


class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('core', '0022_public_favicon'),
    ]

    run_before = [("oauth2_provider", _MIGRATION_NAMES[0])]

    operations = []
    if _original_tables_exist():
        # If original tables existed, we nned to copy data and remove them afterwards
        for OldModel, NewModel in zip(_get_original_models(), _get_new_models()):
            operations.append(migrations.RunSQL(
                f"ALTER TABLE IF EXISTS {OldModel._meta.db_table} RENAME TO {NewModel._meta.db_table};",
                reverse_sql=f"ALTER TABLE IF EXISTS {NewModel._meta.db_table} RENAME TO {OldModel._meta.db_table};",
                # We copy the original migrations here to trick the planner into believing the migrations had run normally
                state_operations=_get_oauth_migration_operations(NewModel._meta.model_name)
            ))
    else:
        # We need to copy Django OAuth Toolkit's migrations here to re-create
        # all models, because we cannot auto-generate them due to the big swappable
        # model circular dependency rabbit hole
        operations += _get_oauth_migration_operations()


if _original_tables_exist():
    # Fake-unapply migrations so the planner allows run_before
    for name in _MIGRATION_NAMES:
        connection.cursor().execute(f"DELETE FROM django_migrations WHERE app='oauth2_provider' AND name='{name}';")
