package ru.yandex.qe.dispenser.ws.goal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.inside.goals.GoalsClient;
import ru.yandex.inside.goals.model.Goal;
import ru.yandex.qe.dispenser.domain.dao.goal.BaseGoal;
import ru.yandex.qe.dispenser.domain.dao.goal.GoalDao;
import ru.yandex.qe.dispenser.domain.dao.goal.OkrAncestors;

public class GoalSyncTask {

    private static final Logger LOG = LoggerFactory.getLogger(GoalSyncTask.class);
    private static final int PAGE_SIZE = 500;
    private static final int TRACKER_PAGE_SIZE = 50;

    private final GoalDao goalDao;
    private final GoalsClient goalsClient;
    private final boolean syncFromTracker;
    private final TrackerGoalClient trackerGoalClient;
    private final TrackerGoalHelper trackerGoalHelper;

    public GoalSyncTask(final GoalDao goalDao, final GoalsClient goalsClient,
                        final boolean syncFromTracker, final TrackerGoalClient trackerGoalClient,
                        final TrackerGoalHelper trackerGoalHelper) {
        this.goalsClient = goalsClient;
        this.goalDao = goalDao;
        this.syncFromTracker = syncFromTracker;
        this.trackerGoalClient = trackerGoalClient;
        this.trackerGoalHelper = trackerGoalHelper;
    }

    public void updateWithHealthCheck() {
        LOG.info("Start Goal sync");
        update();
        LOG.info("Finish Goal sync");
    }

    public void update() {
        if (syncFromTracker) {
            syncGoalsFromTracker();
        } else {
            syncGoalsFromGoals();
        }
    }

    private void syncGoalsFromGoals() {
        LOG.info("Syncing goals from goals backend...");
        try {
            goalsClient.getGoalsService().getGoalsBuilder().perPage(PAGE_SIZE).getPaginatedGoals()
                    .forEachRemaining(listResult -> {
                        LOG.debug("Goals page loaded");

                        final List<BaseGoal> goals = listResult.stream()
                                .map(this::toDomainGoal)
                                .collect(Collectors.toList());

                        LOG.debug("Goals page upserted");
                        goalDao.upsert(goals);
                    });
            LOG.info("Successfully synced goals from goals backend");
        } catch (RuntimeException e) {
            LOG.error("Error syncing goals", e);
        }
    }

    private static class GoalsTree {
        private final Map<Long, String> parentIssueIdById = new HashMap<>();
        private final Map<String, Long> goalIdByIssueId = new HashMap<>();
        private final Set<Long> okrGoals = new HashSet<>();

        private final Map<Long, long[]> parentOkrsByGoalId = new HashMap<>();

        public void addOkrGoal(final long id) {
            okrGoals.add(id);
        }

        public void addLink(final long id, final String parentIssueId) {
            parentIssueIdById.put(id, parentIssueId);
        }

        public Set<Long> getGoalIds() {
            return Sets.union(okrGoals, parentIssueIdById.keySet());
        }

        public long[] getOkrAncestors(final Long goalId) {
            return getOkrAncestors(goalId, null);
        }

        private long[] getOkrAncestors(final Long goalId, Set<Long> processedGoals) {
            final long[] cached = parentOkrsByGoalId.get(goalId);
            if (cached != null) {
                return cached;
            }

            if (goalId == null) {
                return ArrayUtils.EMPTY_LONG_ARRAY;
            }
            if (processedGoals == null) {
                processedGoals = new HashSet<>();
            }
            processedGoals.add(goalId);

            final boolean isOkr = okrGoals.contains(goalId);
            final Long parentId = goalIdByIssueId.get(parentIssueIdById.get(goalId));
            if (processedGoals.contains(parentId)) {
                LOG.error("Cyclic goal depends in path {}", processedGoals);
                return ArrayUtils.EMPTY_LONG_ARRAY;
            }

            final long[] parentOkrs = getOkrAncestors(parentId, processedGoals);
            final long[] result;
            if (parentOkrs.length < 3 && isOkr) {
                result = Arrays.copyOf(parentOkrs, parentOkrs.length + 1);
                result[parentOkrs.length] = goalId;
            } else {
                result = parentOkrs;
            }

            parentOkrsByGoalId.put(goalId, result);

            return result;
        }

        public void addGoalId(final String issueId, final long goalId) {
            goalIdByIssueId.put(issueId, goalId);
        }
    }

    private void syncGoalsFromTracker() {
        LOG.info("Syncing goals from tracker...");

        final GoalsTree goalsTree = new GoalsTree();

        trackerGoalClient.findGoalIssues()
                .paginate(TRACKER_PAGE_SIZE)
                .forEachRemaining(issuesPage -> {
                    final List<BaseGoal> goals = new ArrayList<>(issuesPage.size());
                    for (final GoalIssue issue : issuesPage) {
                        final BaseGoal domainGoal = toDomainGoal(issue);
                        goals.add(domainGoal);

                        final long goalId = getGoalId(issue.getKey());
                        if (domainGoal.getImportance() == Goal.Importance.OKR) {
                            goalsTree.addOkrGoal(goalId);
                        }

                        goalsTree.addGoalId(issue.getId(), goalId);

                        for (final GoalIssue.LocalLinkRef link : issue.getLinks()) {
                            if (link.getRelationship().equals("is dependent by")) {
                                goalsTree.addLink(goalId, link.getIssueId());
                                break;
                            }
                        }
                    }
                    goalDao.upsert(goals);
                });

        final List<List<Long>> parts = Lists.partition(Lists.newArrayList(goalsTree.getGoalIds()), 1000);
        for (final List<Long> ids : parts) {
            final Map<Long, ru.yandex.qe.dispenser.domain.dao.goal.Goal> goalById = goalDao.read(ids);
            for (final Long goalId : ids) {
                final ru.yandex.qe.dispenser.domain.dao.goal.Goal goal = goalById.get(goalId);
                if (goal == null) {
                    LOG.error("Missing goal with id {}", goalId);
                    continue;
                }
                final OkrAncestors okrAncestors = toOkrParents(goalsTree.getOkrAncestors(goalId));
                if (!okrAncestors.equals(goal.getOkrParents())) {
                    goalDao.updateOrkAncestors(goalId, okrAncestors);
                }

            }
        }

        LOG.info("Successfully synced goals from tracker");
    }

    @NotNull
    private OkrAncestors toOkrParents(final long[] okrAncestorsIds) {
        final EnumMap<OkrAncestors.OkrType, Long> goalIdByType = new EnumMap<>(OkrAncestors.OkrType.class);
        goalIdByType.put(OkrAncestors.OkrType.VALUE_STREAM, okrAncestorsIds.length > 0 ? okrAncestorsIds[0] : null);
        goalIdByType.put(OkrAncestors.OkrType.UMBRELLA, okrAncestorsIds.length > 1 ? okrAncestorsIds[1] : null);
        goalIdByType.put(OkrAncestors.OkrType.CONTOUR, okrAncestorsIds.length > 2 ? okrAncestorsIds[2] : null);
        return new OkrAncestors(goalIdByType);
    }

    private BaseGoal toDomainGoal(final GoalIssue issue) {
        final String title = StringUtils.isNotEmpty(issue.getSummary()) ? issue.getSummary() : "No title";
        final long id = getGoalId(issue.getKey());
        final ru.yandex.inside.goals.model.Goal.Importance importance = getGoalImportance(issue);
        final ru.yandex.inside.goals.model.Goal.Status status = getGoalStatus(issue);
        return new BaseGoal(id, title, importance, status);
    }

    private long getGoalId(final String issueKey) {
        final String[] keyParts = issueKey.split("-");
        if (keyParts.length != 2) {
            throw new IllegalArgumentException("Unexpected issue key: " + issueKey);
        }
        return Long.parseLong(keyParts[1]);
    }

    private ru.yandex.inside.goals.model.Goal.Importance getGoalImportance(final GoalIssue issue) {
        final GoalIssue.GoalImportance goalImportance = issue.getImportance();
        if (goalImportance != null) {
            final Goal.Importance importance = trackerGoalHelper.getImportanceById(goalImportance.getId());
            if (importance != null) {
                return importance;
            }
        }
        return ru.yandex.inside.goals.model.Goal.Importance.UNKNOWN;
    }

    private ru.yandex.inside.goals.model.Goal.Status getGoalStatus(final GoalIssue issue) {
        switch (StringUtils.defaultString(issue.getStatus().getKey())) {
            case "asPlanned":
                return ru.yandex.inside.goals.model.Goal.Status.PLANNED;
            case "withRisks":
                return ru.yandex.inside.goals.model.Goal.Status.RISK;
            case "blockedGoal":
                return ru.yandex.inside.goals.model.Goal.Status.BLOCKED;
            case "cancelled":
                return ru.yandex.inside.goals.model.Goal.Status.CANCELLED;
            case "achieved":
                return ru.yandex.inside.goals.model.Goal.Status.REACHED;
            case "newGoal":
                return ru.yandex.inside.goals.model.Goal.Status.NEW;
            default:
                return ru.yandex.inside.goals.model.Goal.Status.UNKNOWN;
        }
    }

    private BaseGoal toDomainGoal(final ru.yandex.inside.goals.model.Goal goal) {
        return new BaseGoal(goal.getId(), goal.getTitle().orElse("No title"), goal.getImportance(), goal.getStatus());
    }

}
