from patch.include import *
from tqdm import tqdm


class NotConvertibleBqlError(Exception):
    pass


def convert_operator(operator):
    if operator in ('LIKE', 'ILIKE'):
        return '~'
    elif operator in ('NOT LIKE', 'NOT ILIKE'):
        return '!~'
    elif operator == '==':
        return '='
    elif operator in ('<>', '><'):
        return '!='
    elif operator in ('MEMBER_OF', 'EXISTS'):
        raise NotConvertibleBqlError
    else:
        return operator.lower()


def convert_value(value, operator=None):
    if isinstance(value, list):
        values = ', '.join(convert_value(v) for v in value)
        return f'({values})'
    elif isinstance(value, str):
        if value == '__G_CURRENT_USER':
            return 'currentUser()'
        elif value == '__G_DATE':
            # Хак, т.к. нет сравнения дат
            return '-12h'
        elif value == '__G_NOW ':
            return '0d'
        elif operator == '~':
            return value.strip('%')
        else:
            value = value.replace('now()', '')
            return f'"{value.strip()}"'
    elif value is None:
        return 'NULL'
    return value


def convert_field(field):
    if field in ('parent', 'parent_id'):
        field = 'project'
    elif field == 'op_gantt_task.sched_start_date:':
        field = 'plan_start_date'
    elif field == 'op_gantt_task.sched_end_date:':
        field = 'plan_end_date'
    if '.' in field:
        # status.code
        raise NotConvertibleBqlError
    return field


def convert_bql(bql, depth=0):
    if not isinstance(bql, list):
        return

    if len(bql) == 3 and isinstance(bql[0], str) and bql[0] not in ('OR', 'AND'):
        field = convert_field(bql[0])
        operator = bql[1]
        value = bql[2]

        if isinstance(value, list) and not value:
            # ignore as broken: [field, in, []]
            # !!! [field, in, []] match nothing, but after remove - match all!
            return

        operator = convert_operator(operator)
        value = convert_value(value, operator=operator)
        return f'{field} {operator} {value}'

    queryset = []
    operator = 'and'
    for item in bql:
        if item in ('OR', 'AND'):
            operator = item.lower()
            continue
        else:
            jql = convert_bql(item, depth + 1)
            if jql:
                queryset.append(jql)
    query = f' {operator} '.join(queryset)
    if query:
        return f'({query})' if depth > 0 else query


@app_context(commit=False)
def convert_bql_to_ubql2_fix():
    """
    Для тестирования патча: ( cd /opt/eva-app; python3 -m patch.202401241621_convert_bql_to_ubql2_fix )
    Здесь можно работать с моделями через models.CmfTask и т.д.
    Для прогрессбара используйте:
    for task in tqdm(models.CmfTask.list()):
        ...
    """
    is_test = '--test' in sys.argv
    print('Запуск патча convert_bql_to_ubql2_fix', 'test_mode' if is_test else '')
    count = 0
    converted = 0
    errors = 0
    passed = 0

    with cmfutil.disable_acl(), cmfutil.disable_notify():
        uniq_bql = set()
        task_filters = models.CmfTaskFilter.list(
            filter=[['bql', 'ILIKE', '[_%]']],
            fields=['bql', 'ubql2']
        )
        if not is_test:
            task_filters = tqdm(task_filters)

        for task_filter in task_filters:
            count += 1
            if is_test:
                bql_key = re.sub(
                    r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
                    '00000000-0000-0000-0000-000000000000', task_filter.bql.value)
                if bql_key in uniq_bql:
                    continue
                uniq_bql.add(bql_key)
                print(task_filter.id)
                print('BQL     ', task_filter.bql)
                print('UBQL_OLD', task_filter.ubql2)

            try:
                # На бцрм есть фильтры без внешних скобок: [...], [...], ...
                #  можно пробовать исправлять, но может это не актуальный легаси.
                # поле bql - строковое, не json!!!
                sbql = task_filter.bql.value
                try:
                    bql = json.loads(sbql)
                except ValueError as e:
                    if 'Expecting value' in str(e):
                        print(f'Try convert quotes in task_filter.bql: {sbql}')
                        sbql = sbql.replace("'", '"')
                        bql = json.loads(sbql)
                        # Опасно менять исходник
                        task_filter.bql = sbql
                        # print(f'BQL({type(bql)}, {task_filter.bql.is_changed}): {task_filter.bql}')
                    elif 'Extra data' in str(e):
                        print(f'Try add braces and convert quotes in task_filter.bql: {sbql}')
                        sbql = sbql.replace("'", '"')
                        sbql = f'[{sbql}]'
                        bql = json.loads(sbql)
                        # Опасно менять исходник
                        task_filter.bql = sbql
                    else:
                        raise
                ubql = convert_bql(bql)
                if is_test:
                    print('UBQL_NEW', ubql)

                converted += 1
            except NotConvertibleBqlError:
                passed += 1
                print(f'Конвертировать bql в ubql2 не поддерживается: "{task_filter.bql}"')
                ubql = json.dumps(bql)
            except Exception as e:
                errors += 1
                print(f'Не удалось конвертировать bql в ubql2. Некорректные данные в bql({e}): "{task_filter.bql}"')
                ubql = task_filter.ubql2

            if is_test:
                print()
                print()
                print()
                continue
            task_filter.ubql2 = ubql
            task_filter.save(only_data=True, emit=False, notify=False)

            if count % 100 == 0:
                commit_all_ds()
        print(f'Total: {count}, converted: {converted}, passed {passed}, errors: {errors}')
        if not is_test:
            commit_all_ds()


if __name__ == "__main__":
    convert_bql_to_ubql2_fix()
