"""clean_projects_models

Revision: 0205
Revision ID: 90dacf065be7
Revises: e2464817d291
Create Date: 2021-05-24 16:22:47.841626

"""
from alembic import op
import sqlalchemy as sa
import uuid
from sqlalchemy.dialects import postgresql
from datetime import datetime


# revision identifiers, used by Alembic.
revision = '90dacf065be7'
down_revision = 'e2464817d291'
branch_labels = None
depends_on = None

projects = sa.Table(
    'projects',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('owner_id', sa.Integer),
    sa.Column('created_by_id', sa.Integer)
)

cards = sa.Table(
    'cards',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('project_id', sa.Integer),
    sa.Column('parent_id', sa.Integer),
    sa.Column('default_child_card_model_id', sa.Integer),
    sa.Column('cache_id', sa.String),
    sa.Column('model_id', sa.Integer)
)

card_models = sa.Table(
    'card_models',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('name', sa.String),
    sa.Column('owner_id', sa.Integer),
    sa.Column('created_in_id', sa.Integer),
    sa.Column('is_project_specific', sa.String),
    sa.Column('attribute_group_id', sa.Integer),
    sa.Column('copied_from_id', sa.Integer),
    sa.Column('indicator', sa.String),
    sa.Column('card_color', sa.String),
    sa.Column('cache_id', sa.String)
)

enabled_models = sa.Table(
    'project_enabled_models',
    sa.MetaData(),
    sa.Column('project_id', sa.Integer),
    sa.Column('model_id', sa.Integer)
)

attribute_groups = sa.Table(
    'attribute_groups',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('owner_id', sa.Integer),
    sa.Column('created_at', sa.DateTime),
    sa.Column('shared_context', sa.PickleType),
    sa.Column('containers', postgresql.JSONB())
)

attributes = sa.Table(
    'attributes',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('card_model_id', sa.Integer),
    sa.Column('card_id', sa.Integer),
    sa.Column('name', sa.String),
    sa.Column('description', sa.String),
    sa.Column('type_name', sa.String),
    sa.Column('container', sa.String),
    sa.Column('position', sa.Integer),
    sa.Column('options', sa.PickleType),
    sa.Column('default_value', sa.PickleType),
    sa.Column('is_from_builtin', sa.Boolean),
    sa.Column('show_on_tile', sa.Boolean),
    sa.Column('group_id', sa.Integer),
    sa.Column('is_pii', sa.Boolean)
)

group_types = sa.Table(
    'card_group_types',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('created_at', sa.DateTime),
    sa.Column('created_by_id', sa.Integer),
    sa.Column('owner_id', sa.Integer),
    sa.Column('name', sa.String),
    sa.Column('description', sa.Text),
    sa.Column('scope', sa.String),
    sa.Column('one_group_per_card', sa.Boolean),
    sa.Column('singular_name', sa.String),
    sa.Column('plural_name', sa.String),
    sa.Column('icon', sa.String),
    sa.Column('builtin', sa.String),
    sa.Column('display_on_card', sa.String),
    sa.Column('auto_random_color', sa.Boolean),
    sa.Column('cache_id', sa.String),
    sa.Column('auto_enabled_on_new_projects', sa.Boolean),
    sa.Column('is_project_specific', sa.Boolean),
    sa.Column('auto_assign_period', sa.String),
    sa.Column('attribute_id', sa.Integer)
)

groups = sa.Table(
    'card_groups',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('created_at', sa.DateTime),
    sa.Column('created_by_id', sa.Integer),
    sa.Column('group_type_id', sa.Integer),
    sa.Column('position', sa.Integer),
    sa.Column('title', sa.String),
    sa.Column('description', sa.String),
    sa.Column('is_archived', sa.Boolean),
    sa.Column('archived_at', sa.DateTime),
    sa.Column('meta', sa.PickleType),
    sa.Column('color', sa.String),
    sa.Column('icon', sa.String),
    sa.Column('show_flow_indicator', sa.Boolean),
    sa.Column('card_state', sa.String),
    sa.Column('default_card_model_id', sa.Integer),
    sa.Column('max_cards_limit', sa.Integer),
    sa.Column('disallow_direct_card_creation', sa.Boolean),
    sa.Column('start_date', sa.Date),
    sa.Column('end_date', sa.Date),
    sa.Column('always_show_description', sa.Boolean),
    sa.Column('parent_id', sa.Integer),
    sa.Column('cache_id', sa.String)
)

cgc = sa.Table(
    'card_groups_cards',
    sa.MetaData(),
    sa.Column('card_id', sa.Integer),
    sa.Column('group_id', sa.Integer),
    sa.Column('position', sa.Integer)
)

card_attributes = sa.Table(
    'card_attributes',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('card_id', sa.Integer),
    sa.Column('attribute_id', sa.Integer)
)

activities = sa.Table(
    'activities',
    sa.MetaData(),
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('project_id', sa.Integer),
    sa.Column('object_type', sa.String),
    sa.Column('object_id', sa.Integer),
    sa.Column('target_type', sa.String),
    sa.Column('target_id', sa.Integer)
)

def get_from_list(elements, props, values):
    ret = None
    for _el in elements:
        for i in range(len(props)):
            ret = _el
            if _el[props[i]] != values[i]:
                ret = None
                break
        if ret:
            return ret
    return None

def upgrade():
    update_cards = []
    update_cards2 = []
    update_gts = []
    update_card_attributes = []
    update_cgc = []
    delete_cgc = []
    update_activities = []

    conn = op.get_bind()

    # look for projects where there is model ids in default_child_card_model_id that are not enabled in the project
    # this is the result of a bug during copy/move operations
    models_used_as_default_child_card_model_but_not_enabled_query = sa.select([
        card_models.c.id,
        card_models.c.name,
        card_models.c.owner_id,
        card_models.c.is_project_specific,
        card_models.c.attribute_group_id,
        card_models.c.indicator,
        card_models.c.card_color,
        projects.c.id,
        projects.c.owner_id,
        projects.c.created_by_id,
        cards.c.id
    ]).select_from(
        card_models\
            .join(cards, cards.c.default_child_card_model_id==card_models.c.id)\
            .join(projects, projects.c.id==cards.c.project_id)
    ).where(
        card_models.c.id.notin_(
            sa.select([enabled_models.c.model_id]).where(enabled_models.c.project_id==projects.c.id)
        )
    )

    for row in conn.execute(models_used_as_default_child_card_model_but_not_enabled_query):
        model_id = row[0]
        model_name = row[1]
        model_owner_id = row[2]
        model_is_project_specific = row[3]
        model_attribute_group_id = row[4]
        model_indicator = row[5]
        model_color = row[6]
        project_id = row[7]
        project_owner_id = row[8]
        project_created_by_id = row[9]
        card_id = row[10]

        if not model_is_project_specific and model_owner_id == project_owner_id:
            conn.execute(enabled_models.insert().values(project_id=project_id, model_id=model_id))
            conn.execute(card_models.update().where(card_models.c.id==model_id).values(cache_id=uuid.uuid4()))

        else:
            other_model_id = None
            other_model_attribute_group_id = None
            other_model_attrs = []

            # we assume that a model with the same name in the project can be used as replacement
            same_name_replacement_model = conn.execute(sa.select([
                card_models.c.id,
                card_models.c.attribute_group_id
            ]).select_from(
                card_models.join(enabled_models, enabled_models.c.model_id==card_models.c.id)
            ).where(
                sa.and_(card_models.c.name==model_name, enabled_models.c.project_id==project_id)
            )).first()

            if same_name_replacement_model:
                # replace model with one with same name in project and rewire attributes on cards
                other_model_id = same_name_replacement_model[0]
                other_model_attribute_group_id = same_name_replacement_model[1]
                for attr in conn.execute(attributes.select().where(attributes.c.group_id == other_model_attribute_group_id)):
                    other_model_attrs.append({"id": attr[0], "name": attr[3], "type_name": attr[5]})

            else:
                # no replacement model found, we create a new project specific model as a copy of the original model
                attr_group = conn.execute(attribute_groups.select().where(attribute_groups.c.id == model_attribute_group_id)).first()
                if attr_group:
                    attr_group_infos = conn.execute(attribute_groups.insert().values(
                        owner_id=project_owner_id,
                        created_at=datetime.utcnow(),
                        shared_context=attr_group[3],
                        containers=attr_group[4]))
                    other_model_attribute_group_id = attr_group_infos.inserted_primary_key[0]
                    other_model_infos = conn.execute(card_models.insert().values( name=model_name,
                                                                            owner_id=project_owner_id,
                                                                            created_in_id=project_id,
                                                                            is_project_specific=True,
                                                                            attribute_group_id=other_model_attribute_group_id,
                                                                            copied_from_id=model_id,
                                                                            indicator=model_indicator,
                                                                            card_color=model_color,
                                                                            cache_id = uuid.uuid4()))
                    other_model_id = other_model_infos.inserted_primary_key[0]
                    conn.execute(enabled_models.insert().values(project_id=project_id, model_id=other_model_id))

            # other_model_id can remain None after the above steps if the original model was deleted

            update_cards.append({'card_id': card_id, 'default_child_card_model_id': other_model_id, 'cache_id': uuid.uuid4()})

            # rewire or re-create attributes in cards after model change above
            for attr in conn.execute(attributes.select().where(attributes.c.group_id == model_attribute_group_id)):
                other_model_attr = get_from_list(other_model_attrs, ["name", "type_name"], [attr[3], attr[5]])
                other_attribute_id = None
                if other_model_attr:
                    other_attribute_id = other_model_attr["id"]
                    update_card_attributes.append({'old_attr_id': attr[0], 'new_attr_id': other_model_attr["id"], '_project_id': project_id})
                else:
                    attr_infos = conn.execute(attributes.insert().values(
                        card_model_id = attr[1],
                        card_id = attr[2],
                        name = attr[3],
                        description = attr[4],
                        type_name = attr[5],
                        container = attr[6],
                        position = attr[7],
                        options = attr[8],
                        default_value = attr[9],
                        is_from_builtin = attr[10],
                        show_on_tile = attr[11],
                        group_id = other_model_attribute_group_id,
                        is_pii = attr[13]))
                    other_attribute_id = attr_infos.inserted_primary_key[0]
                    other_model_attrs.append({"id": other_attribute_id, "name": attr[3], "type_name": attr[5]})

                if attr[5] == 'group_type':
                    group_type = conn.execute(group_types.select().where(group_types.c.attribute_id == attr[0])).first()
                    other_group_type_id = None
                    other_groups = []
                    if other_model_attr:
                        other_group_type_id = conn.execute(group_types.select().where(group_types.c.attribute_id == other_model_attr["id"])).first()[0]
                        update_gts.append({'gt_id': other_group_type_id, 'attr_id': other_attribute_id, 'cache_id': uuid.uuid4()})
                        for group_info in conn.execute(groups.select().where(groups.c.group_type_id == other_group_type_id)):
                            other_groups.append({'id': group_info[0], 'title': group_info[5]})
                    else:
                        gt_infos = conn.execute(group_types.insert().values(
                            created_at = datetime.utcnow(),
                            created_by_id = project_created_by_id,
                            owner_id = project_owner_id,
                            name = group_type[4],
                            description = group_type[5],
                            scope = group_type[6],
                            one_group_per_card = group_type[7],
                            singular_name = group_type[8],
                            plural_name = group_type[9],
                            icon = group_type[10],
                            builtin = group_type[11],
                            display_on_card = group_type[12],
                            auto_random_color = group_type[13],
                            cache_id = uuid.uuid4(),
                            auto_enabled_on_new_projects = group_type[15],
                            is_project_specific = group_type[16],
                            auto_assign_period = group_type[17],
                            attribute_id = other_attribute_id))
                        other_group_type_id = gt_infos.inserted_primary_key[0]

                    for group in conn.execute(groups.select().where(groups.c.group_type_id == group_type[0])):
                        other_group = get_from_list(other_groups, ["title"], [group[5]])
                        if other_group:
                            other_group_id = other_group['id']
                            for child in conn.execute(cards.select().where(cards.c.parent_id == card_id)):
                                if conn.execute(cgc.select().where(sa.and_(cgc.c.group_id == other_group_id, cgc.c.card_id == child[0]))).first() is None:
                                    update_cgc.append({'_project_id': project_id, '_card_id': child[0], 'old_group_id': group[0], 'new_group_id': other_group_id})
                                else:
                                    delete_cgc.append({'_card_id': child[0], 'old_group_id': group[0]})
                        else:
                            g_infos = conn.execute(groups.insert().values(
                                created_at = datetime.utcnow(),
                                created_by_id = project_created_by_id,
                                group_type_id = other_group_type_id,
                                position = group[4],
                                title = group[5],
                                description = group[6],
                                is_archived = group[7],
                                archived_at = group[8],
                                meta = group[9],
                                color = group[10],
                                icon = group[11],
                                show_flow_indicator = group[12],
                                card_state = group[13],
                                default_card_model_id = group[14],
                                max_cards_limit = group[15],
                                disallow_direct_card_creation = group[16],
                                start_date = group[17],
                                end_date = group[18],
                                always_show_description = group[19],
                                parent_id = card_id,
                                cache_id = uuid.uuid4()
                            ))
                            other_group_id = g_infos.inserted_primary_key[0]
                            for child in conn.execute(cards.select().where(cards.c.parent_id == card_id)):
                                update_cgc.append({'_project_id': project_id, '_card_id': child[0], 'old_group_id': group[0], 'new_group_id': other_group_id})

                update_card_attributes.append({'old_attr_id': attr[0], 'new_attr_id': other_attribute_id, '_project_id': project_id})

            # note that if a replacement model could not be created, cards will lose their model
            update_cards2.append({'_project_id': project_id, 'old_model_id': model_id, 'new_model_id': other_model_id})

    if update_cgc:
        conn.execute(cgc.update().where(
            sa.and_(cgc.c.card_id==sa.bindparam('_card_id'), cgc.c.group_id==sa.bindparam('old_group_id'))
        ).values(group_id=sa.bindparam('new_group_id')), update_cgc)
        conn.execute(activities.update().where(
            sa.and_(activities.c.project_id==sa.bindparam('_project_id'),
                    activities.c.object_type=='Card',
                    activities.c.target_type=='CardGroup',
                    activities.c.target_id==sa.bindparam('old_group_id'))
        ).values(target_id=sa.bindparam('new_group_id')), update_cgc)

    if delete_cgc:
        stmt = cgc.delete().where(sa.and_(cgc.c.group_id == sa.bindparam('old_group_id'), cgc.c.card_id==sa.bindparam('_card_id')))
        conn.execute(stmt, delete_cgc)

    if update_gts:
        conn.execute(group_types.update().where(group_types.c.id==sa.bindparam('gt_id')).values(attribute_id=sa.bindparam('attr_id'), cache_id=sa.bindparam('cache_id')), update_gts)

    if update_cards:
        conn.execute(cards.update().where(cards.c.id==sa.bindparam('card_id')).values(default_child_card_model_id=sa.bindparam('default_child_card_model_id'), cache_id=sa.bindparam('cache_id')), update_cards)

    if update_cards2:
        conn.execute(cards.update().where(
            sa.and_(cards.c.project_id==sa.bindparam('_project_id'), cards.c.model_id==sa.bindparam('old_model_id'))
        ).values(model_id=sa.bindparam('new_model_id'), cache_id=sa.func.uuid_generate_v4()), update_cards2)

    if update_card_attributes:
        conn.execute(card_attributes.update().where(
            sa.and_(card_attributes.c.card_id==cards.c.id, cards.c.project_id==sa.bindparam('_project_id'), card_attributes.c.attribute_id==sa.bindparam('old_attr_id'))
        ).values(attribute_id=sa.bindparam('new_attr_id')), update_card_attributes)
        conn.execute(activities.update().where(
            sa.and_(activities.c.project_id==sa.bindparam('_project_id'), activities.c.object_type=='Attribute', activities.c.object_id==sa.bindparam('old_attr_id'))
        ).values(object_id=sa.bindparam('new_attr_id')), update_card_attributes)

    conn.execute("""
        WITH positions as (
            SELECT attributes.id, row_number() OVER (partition by attributes.group_id, attributes.container order by position) AS p
            FROM attributes JOIN card_models ON card_models.attribute_group_id = attributes.group_id
        )
        UPDATE attributes SET position = positions.p - 1 FROM positions
        WHERE positions.id = attributes.id
    """)
    conn.execute("""UPDATE card_models SET cache_id = uuid_generate_v4()""")


def downgrade():
    pass
