from patch.include import *


def find_additional_parent_loops(task_codes_list=None):

    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 = {}

    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]))
    else:
        print('Циклов не обнаружено')


@app_context(commit=True)
def find_gantt_parent_loops():
    """
    Для запуска патча: ( cd /opt/eva-app; python3 -m patch.find_gantt_parent_loops )
    options:
    --code TASK_CODE   Код задачи, от которой стартовать поиск циклов (необязательный, можно указать несколько раз)
    """
    parser = argparse.ArgumentParser(description="Поиск циклов по связям родитель/дочерняя, доп.родитель/доп.дочерняя")
    parser.add_argument(
        "--code",
        dest="task_codes_list",
        action="append",
        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)

if __name__ == "__main__":
    find_gantt_parent_loops()
