package ru.yandex.webmaster3.storage.takeout;

import lombok.RequiredArgsConstructor;
import lombok.With;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;
import ru.yandex.webmaster3.storage.util.yt.*;

import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

/**
 * @author leonidrom
 */
@Service
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class YtUserDataDeleteService {
    private static RetryUtils.RetryPolicy YDB_RETRY_POLICY = RetryUtils.linearBackoff(5, Duration.standardSeconds(30));

    private final ForkJoinPool forkJoinPool = new ForkJoinPool(32);
    private final YtService ytService;
    private final YtUserDataDeleteQueueYDao ytUserDataDeleteQueueYDao;
    private final YtUserDataDirsYDao ytUserDataDirsYDao;

    // Удаляет привязанные к user id записи из Yt
    public void deleteUserData(UUID requestId, long userId) {
        // получим каталоги, в которых есть таблицы с пользовательскими данными
        var dirList = ytUserDataDirsYDao.getAll();
        log.info("Starting scraping Yt tables.");

        // найдем в них таблицы, из которых надо будет удалить записи
        var tables = scrapeForUserDataTables(dirList);
        log.info("Tables with users data: {}", Arrays.toString(tables.toArray()));

        var delReqs = tables.stream()
                .map(tablePath -> new YtUserDataDeleteQueueYDao.Record(
                        requestId,
                        userId,
                        tablePath,
                        DateTime.now(),
                        false))
                .toList();

        // сохраним записи в очереди, далее их подхватит DeleteYtUserDataPeriodicTask
        try {
            RetryUtils.execute(YDB_RETRY_POLICY, () -> ytUserDataDeleteQueueYDao.add(delReqs));
        } catch (InterruptedException e) {
            throw new WebmasterYdbException(e);
        }
    }

    private List<YtPath> scrapeForUserDataTables(List<YtPath> scrapeDirs) {
        List<YtPath> userDataTables = new ArrayList<>();
        try {
            ytService.withoutTransaction(cypressService -> {
                scrapeDirs.forEach(scrapeDir -> {
                    log.info("Started scraping: {}", scrapeDir);
                    var dirUserDataTables = cypressService.list(scrapeDir).stream()
                            .map(cypressService::getNode)
                            .filter(n -> n.getNodeType() == YtNode.NodeType.TABLE)
                            .map(n -> (YtTable)n)
                            .filter(YtUserDataDeleteService::isUserDataTable)
                            .map(YtNode::getPath)
                            .toList();
                    userDataTables.addAll(dirUserDataTables);
                    log.info("Finishes scraping {}, total tables in dir: {}", scrapeDir, dirUserDataTables.size());
                });

                return true;
            });
        } catch (InterruptedException e) {
            log.error("Exception", e);
        }

        return userDataTables;
    }

    public List<YtPath> scrapeForUserDataDirs(YtPath startDir) {
        Set<YtPath> dirsWithUserData = new ConcurrentHashSet<>();
        try {
            ytService.withoutTransaction(cypressService -> {
                forkJoinPool.invoke(new YtDirsScraper(cypressService, startDir, dirsWithUserData));
                return true;
            });
        } catch (InterruptedException e) {
            log.error("Exception", e);
        }

        return new ArrayList<>(dirsWithUserData);
    }

    @RequiredArgsConstructor
    private static class YtDirsScraper extends RecursiveAction {
        private static final String[] SKIP_DIR_SUBPATHS = {
            "tmp/mdb",
        };

        private final YtCypressService cypressService;
        @With
        private final YtPath scrapePath;
        private final Set<YtPath> userDataDirs;

        @Override
        protected void compute() {
            if (!cypressService.exists(scrapePath)) {
                // нода могла уже удалиться
                log.info("Node no longer exists: {}", scrapePath);
                return;
            }

            YtNode node;
            try {
                node = cypressService.getNode(scrapePath);
            } catch (Exception e) {
                log.error("Error getting node: {}", scrapePath, e);
                return;
            }

            if (node.getNodeType() == YtNode.NodeType.MAP_NODE) {
                scrapeDir();
            } else if (node.getNodeType() == YtNode.NodeType.TABLE) {
                scrapeTable(node);
            }
        }

        private void scrapeDir() {
            if (shouldSkipDir()) {
                return;
            }

            var actionsList = cypressService.list(scrapePath).stream()
                    .map(this::withScrapePath)
                    .toList();

            if (!actionsList.isEmpty()) {
                invokeAll(actionsList);
            }
        }

        private boolean shouldSkipDir() {
            String dirStr = scrapePath.toString();
            for (String skipSubpath : SKIP_DIR_SUBPATHS) {
                if (dirStr.contains(skipSubpath)) {
                    return true;
                }
            }

            return false;
        }

        private void scrapeTable(YtNode node) {
            YtPath dirPath = scrapePath.getParent();
            if (userDataDirs.contains(dirPath)) {
                return;
            }

            YtTable table = (YtTable)node;
            boolean isUserDataTable = isUserDataTable(table);
            if (isUserDataTable) {
                log.info("User data dir: {}", dirPath);
                userDataDirs.add(dirPath);
            }
        }
    }

    private static boolean isUserDataTable(YtTable table) {
        var schema = table.getSchema();
        if (schema == null) {
            return false;
        }

        var columns = schema.getColumns();
        return columns.stream().anyMatch(YtUserDataDeleteService::isUserIdColumn);
    }

    private static boolean isUserIdColumn(YtColumn<?> col) {
        return col.getName().equalsIgnoreCase("user_id")
                || col.getName().equalsIgnoreCase("userid")
                || col.getName().equalsIgnoreCase("uid");
    }
}
