from patch.include import *


def find_additional_parent_loops(task_codes_list=None, break_relations=False):

    def has_additional_parent_loop(start_node):
        visited_nodes = set()
        stack = []
        stack_set = set()
        results = set()

        def get_parent_ids(gantt_task_id):
            global add_cache, hit_cache
            if gantt_task_id in parent_cache:
                return parent_cache[gantt_task_id]
            parent_ids = []
            task = models.CmfTask.sget(filter=['op_gantt_task_id', '==', gantt_task_id], fields=['parent_task.op_gantt_task_id'])
            if task and task.parent_task:
                parent_ids.append(task.parent_task.op_gantt_task_id)
            _filter = [['out_link.op_gantt_task_id', '=', gantt_task_id], ['relation_type.code', '=', 'system.additional_parent']]
            additional_parents_relations = models.CmfRelationOption.slist(filter=_filter, fields=['in_link.op_gantt_task_id'])
            if additional_parents_relations:
                additional_parent_ids = [rel.in_link.op_gantt_task_id for rel in additional_parents_relations]
                parent_ids.extend(additional_parent_ids)
            parent_cache[gantt_task_id] = parent_ids
            return parent_ids

        def dfs(node):
            if node in stack_set:
                cycle_start_index = stack.index(node)
                cycle_ids = tuple(stack[cycle_start_index:] + [node])
                return cycle_ids
            if node in visited_nodes:
                return False
            visited_nodes.add(node)
            stack.append(node)
            stack_set.add(node)
            for neighbor in get_parent_ids(node):
                result = dfs(neighbor)
                if result:
                    results.add(result)
            stack.pop()
            stack_set.remove(node)
            return False

        dfs(start_node.out_link.op_gantt_task.id.value)
        return results

    BATCH_SIZE = 50
    base_filter = ['relation_type.code', '==', 'system.additional_parent']
    filter_for_relations = [base_filter]

    if task_codes_list:
        filter_for_relations.append(['OR', ['in_link.code', 'IN', task_codes_list], ['out_link.code', 'IN', task_codes_list]])

    total = models.CmfRelationOption.count(filter=filter_for_relations, fields=['out_link.op_gantt_task'])
    visited_tasks = set()
    offset = 0
    processed_relations = 0
    results = set()
    parent_cache = {}

    broken_links = []
    not_broken_links = []

    while True:
        relations_batch = models.CmfRelationOption.list(filter=filter_for_relations, fields=['out_link.op_gantt_task'], slice=[offset, offset + BATCH_SIZE])
        if not relations_batch:
            break

        for rel in relations_batch:
            processed_relations += 1
            tid = rel.out_link.id.value
            if tid in visited_tasks:
                continue
            res = has_additional_parent_loop(rel)
            if res:
                results.update(res)
            visited_tasks.add(tid)

        print(f'Обработанно связей: {processed_relations}/{total} ({processed_relations/total:.0%})')
        offset += BATCH_SIZE

    if results:
        for i, result in enumerate(results, start=1):
            print(i, 'Найден цикл: ', ' -> '.join([models.CmfTask.sget(filter=['op_gantt_task_id', '==', gt_id]).code for gt_id in result]))

        if break_relations:
            for result in results:
                if len(result) < 2:
                    continue
                child_id = result[0]
                parent_id = result[1]

                child_task = models.CmfTask.sget(filter=['op_gantt_task_id', '==', child_id], fields=['code'])
                parent_task = models.CmfTask.sget(filter=['op_gantt_task_id', '==', parent_id], fields=['code'])

                child_code = child_task.code if child_task else str(child_id)
                parent_code = parent_task.code if parent_task else str(parent_id)

                try:
                    rel = models.CmfRelationOption.get(filter=[
                        ['out_link.op_gantt_task_id', '==', child_id],
                        ['in_link.op_gantt_task_id', '==', parent_id],
                        ['relation_type.code', '==', 'system.additional_parent'],
                    ])
                    if not rel:
                        not_broken_links.append((child_code, parent_code, 'Связь не найдена'))
                    else:
                        rel.cmf_deleted = True
                        rel.save(only_data=True)
                        broken_links.append((child_code, parent_code))
                except Exception as e:
                    not_broken_links.append((child_code, parent_code, f'Ошибка: {e}'))

    else:
        print('Циклов не обнаружено')

    if break_relations:
        print('---')
        print('Разрыв связей по флагу --break:')
        if broken_links:
            print('Удалены связи доп.дочерняя -> доп.родитель:')
            for child_code, parent_code in broken_links:
                print(f'  {child_code} -> {parent_code}')

        if not_broken_links:
            print('Не удалось удалить связи доп.дочерняя -> доп.родитель:')
            for child_code, parent_code, reason in not_broken_links:
                print(f'  {child_code} -> {parent_code}: {reason}')



@app_context(commit=True)
def find_gantt_parent_loops():
    """
    Для запуска патча: ( cd /opt/eva-app; python3 -m patch.find_gantt_parent_loops )
    options:
    --code TASK_CODE   Код задачи, от которой стартовать поиск циклов (необязательный, можно указать несколько раз)
    --break            Разорвать связь доп.родитель/доп.дочерняя между первыми двумя задачами в найденном цикле
    """
    parser = argparse.ArgumentParser(description="Поиск циклов по связям родитель/дочерняя, доп.родитель/доп.дочерняя")
    parser.add_argument(
        "--code",
        dest="task_codes_list",
        action="append",
        help="Код задачи, от которой стартовать поиск циклов (можно указать несколько раз)",
    )
    parser.add_argument(
        '--break',
        dest='break_relations',
        action='store_true',
        help='Разорвать связь между первыми двумя задачами в найденном цикле',
    )
    args = parser.parse_args()
    task_codes_list = args.task_codes_list if args.task_codes_list else None

    find_additional_parent_loops(task_codes_list=task_codes_list, break_relations=args.break_relations)

if __name__ == "__main__":
    find_gantt_parent_loops()
