package ru.yandex.qe.dispenser.domain;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.yandex.qe.dispenser.api.DtoBuilder;
import ru.yandex.qe.dispenser.api.v1.DiPersonGroup;
import ru.yandex.qe.dispenser.api.v1.DiProject;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.index.NormalizedPrimaryKeyBase;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;
import ru.yandex.qe.dispenser.domain.util.ValidationUtils;

/**
 * Сервис в ABC, на который выдается {@link Quota квота}. Обычно иерархия сервисов соответствует иерархии в ABC.
 * <p>
 * Note: Для провайдера Nirvana иерархия сервисов не соответствует иерархии ABC.
 */
public final class Project extends NormalizedPrimaryKeyBase<Project.Key> {

    public static final String TRASH_PROJECT_KEY = "__trash__";

    @NotNull
    private final String name;
    @NotNull
    private final String description;
    @Nullable
    private final Integer abcServiceId;
    @Nullable
    private final Project parent;
    @NotNull
    private final Set<Project> subprojects;
    private final boolean removed;
    private final boolean syncedWithAbc;
    @Nullable
    private final String mailList;
    @Nullable
    private final Long valueStreamAbcServiceId;

    @Deprecated
    public Project(@NotNull final String key, @NotNull final String name, @Nullable final Project parent) {
        this(withKey(key).name(name).parent(parent));
    }

    private Project(@NotNull final Builder builder) {
        super(builder.key);
        name = ValidationUtils.requireNonNull(builder.name, "Name required!");
        description = builder.description;
        abcServiceId = builder.abcServiceId;
        parent = builder.parent;
        if (parent != null && !builder.immutable) {
            parent.subprojects.add(this);
        }
        subprojects = builder.subprojects;
        if (builder.id > 0) {
            setId(builder.id);
        }
        removed = builder.removed;
        syncedWithAbc = builder.syncedWithAbc;
        mailList = builder.mailList;
        valueStreamAbcServiceId = builder.valueStreamAbcServiceId;
    }

    @NotNull
    public static Builder withKey(@NotNull final String key) {
        return withKey(Key.of(key));
    }

    @NotNull
    public static Builder withKey(@NotNull final Key key) {
        return new Builder(key);
    }

    public static Builder copyOf(@NotNull final Project project) {
        return withKey(project.getKey())
                .id(project.getId())
                .name(project.getName())
                .description(project.getDescription())
                .parent(project.parent)
                .subprojects(project.getSubprojectsInternal())
                .removed(project.removed)
                .abcServiceId(project.abcServiceId)
                .syncedWithAbc(project.syncedWithAbc);
    }

    @NotNull
    public String getPublicKey() {
        return getKey().getPublicKey();
    }

    @NotNull
    public String getName() {
        return name;
    }

    @NotNull
    public String getDescription() {
        return description;
    }

    @Nullable
    public Integer getAbcServiceId() {
        return abcServiceId;
    }

    @NotNull
    public Project getParent() {
        return Objects.requireNonNull(parent, "This project is root! Please, check this by isRoot()");
    }

    public long getParentId() {
        return !isRoot() ? getParent().getId() : -1;
    }

    @NotNull
    public Set<Project> getSubProjects() {
        return subprojects;
    }

    @NotNull
    public Set<Project> getExistingSubProjects() {
        return subprojects.stream().filter(project -> !project.isRemoved()).collect(Collectors.toSet());
    }

    @NotNull
    public Set<Project> getSubprojectsInternal() {
        return subprojects;
    }

    @NotNull
    public Set<Project> getRealSubprojects() {
        return getExistingSubProjects().stream().filter(Project::isReal).collect(Collectors.toSet());
    }

    @NotNull
    public Set<Project> getPersonalSubprojects() {
        return getExistingSubProjects().stream().filter(Project::isPersonal).collect(Collectors.toSet());
    }

    public boolean isLeaf() {
        return getExistingSubProjects().isEmpty();
    }

    public boolean isPersonal() {
        return !isReal();
    }

    public boolean isReal() {
        return getKey().getPerson() == null;
    }

    public boolean isRealLeaf() {
        return getRealSubprojects().isEmpty();
    }

    public boolean hasPersonalSubprojects() {
        return !getPersonalSubprojects().isEmpty();
    }

    public boolean hasSubprojects() {
        return !getSubProjects().isEmpty();
    }

    @NotNull
    public Person getPerson() {
        return Objects.requireNonNull(getKey().getPerson(), "'" + getKey() + "' is not personal! Please, check this by isPersonal()");
    }

    public boolean equalsOrSon(final @NotNull Project project) {
        return getLeastCommonAncestor(project).equals(project);
    }

    public int compareByTree(final @NotNull Project project) {
        return equals(project) ? 0
                : equalsOrSon(project) ? 1
                : project.equalsOrSon(this) ? -1
                : 0;
    }

    @NotNull
    public Project getLeastCommonAncestor(final @NotNull Project project) {
        final List<Project> path1 = getPathFromRoot();
        final List<Project> path2 = project.getPathFromRoot();
        for (int i = 1; i < path1.size() && i < path2.size(); i++) {
            if (!path1.get(i).equals(path2.get(i))) {
                return path1.get(i - 1);
            }
        }
        return Iterables.getLast(path1.size() < path2.size() ? path1 : path2);
    }

    @NotNull
    public List<Project> getPathToRoot() {
        final List<Project> path = new ArrayList<>();
        for (Project cur = this; cur != null; cur = cur.parent) {
            path.add(cur);
        }
        return path;
    }

    @NotNull
    public List<Project> getPathFromRoot() {
        final List<Project> path = getPathToRoot();
        Collections.reverse(path);
        return path;
    }

    @NotNull
    public List<Project> getPathToAncestor(final Project ancestor) {
        final List<Project> pathToLCA = new ArrayList<>();
        Project curProject = this;
        while (curProject != null && !curProject.equals(ancestor)) {
            pathToLCA.add(curProject);
            curProject = curProject.parent;
        }
        return pathToLCA;
    }

    @JsonIgnore
    public boolean isRoot() {
        return parent == null;
    }


    @JsonIgnore
    public boolean isRemoved() {
        return removed;
    }

    @JsonIgnore
    public boolean isSyncedWithAbc() {
        return syncedWithAbc;
    }

    @NotNull
    public DiProject toView() {
        return toView(true);
    }

    @NotNull
    public DiProject toPersonalView() {
        return toViewBuilder(false).makePersonal(getPerson().getLogin()).build();
    }


    @NotNull
    public DiProject toView(final boolean showPersons) {
        return toViewBuilder(showPersons).build();
    }

    @NotNull
    public DiProject toView(final boolean showPersons, final ProjectSerializationContext context) {
        return toViewBuilder(showPersons, context).build();
    }

    private DiProject.Builder toViewBuilder(final boolean showPersons) {
        return toViewBuilder(showPersons, ProjectSerializationContext.DEFAULT);
    }

    private DiProject.Builder toViewBuilder(final boolean showPersons, final ProjectSerializationContext context) {
        final DiProject.Builder builder = DiProject.withKey(getPublicKey())
                .withName(getName())
                .withDescription(getDescription())
                .withAbcServiceId(getAbcServiceId())
                .withParentProject(!isRoot() ? getParent().getPublicKey() : null);

        if (showPersons) {
            final DiPersonGroup responsibles = DiPersonGroup.builder()
                    .addPersons(CollectionUtils.map(context.getResponsibles(this), Person::getLogin))
                    .build();

            final DiPersonGroup.Builder membersBuilder = DiPersonGroup.builder()
                    .addPersons(CollectionUtils.map(context.getMembers(this), Person::getLogin));

            final Collection<YaGroup> linkedMemberGroups = context.getGroups(this);
            for (final YaGroup group : linkedMemberGroups) {
                membersBuilder.addYaGroups(group.getType(), group.getUrl());
            }

            builder.withResponsibles(responsibles).withMembers(membersBuilder.build());
        }

        return builder.withSubprojects(getRealSubprojects().stream().map(Project::getPublicKey).toArray(String[]::new));
    }

    @NotNull
    @Override
    public String toString() {
        return "Project{" +
                "key='" + getKey() + '\'' +
                ", name='" + getName() + '\'' +
                ", parent.key=" + (parent != null ? parent.getKey() : null) +
                ", removed=" + isRemoved() +
                '}';
    }

    @Nullable
    public String getMailList() {
        return mailList;
    }

    @Nullable
    public Long getValueStreamAbcServiceId() {
        return valueStreamAbcServiceId;
    }

    public static final class Key implements Comparable<Key> {
        @NotNull
        private final String key;
        @Nullable
        private final Person person;

        private final int hash;

        private Key(@NotNull final String key, @Nullable final Person person) {
            this.key = key;
            this.person = person;
            this.hash = 31 * Objects.hashCode(person) + key.hashCode();
        }

        @NotNull
        public static Key of(@NotNull final String publicKey) {
            return of(publicKey, null);
        }

        @NotNull
        public static Key of(@NotNull final String publicKey, @Nullable final Person person) {
            return new Key(publicKey, person);
        }

        @NotNull
        public String getPublicKey() {
            return key;
        }

        @Nullable
        public Person getPerson() {
            return person;
        }

        @Nullable
        private String getPersonLogin() {
            return person != null ? person.getLogin() : null;
        }

        @Override
        public int compareTo(@NotNull final Key key) {
            return new CompareToBuilder()
                    .append(getPublicKey(), key.getPublicKey())
                    .append(getPersonLogin(), key.getPersonLogin())
                    .build();
        }

        @Override
        public boolean equals(@Nullable final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            return key.equals(((Key) o).key) && Objects.equals(person, ((Key) o).person);
        }

        @Override
        public int hashCode() {
            return hash;
        }

        @Override
        public String toString() {
            return "{key='" + key + "', person=" + person + '}';
        }
    }

    public static final class Builder implements DtoBuilder<Project> {
        private long id = -1;
        @NotNull
        private final Key key;
        @Nullable
        private String name;
        @NotNull
        private String description = "";
        @Nullable
        private Integer abcServiceId;
        @Nullable
        private Project parent;
        @NotNull
        private final Set<Project> subprojects = new HashSet<>();
        private boolean immutable;
        private boolean removed = false;
        private boolean syncedWithAbc = false;
        @Nullable
        public String mailList;
        @Nullable
        private Long valueStreamAbcServiceId;

        private Builder(@NotNull final Key key) {
            this.key = key;
        }

        @NotNull
        public Builder id(final long id) {
            this.id = id;
            return this;
        }

        @NotNull
        public Builder name(@NotNull final String name) {
            this.name = name;
            return this;
        }

        @NotNull
        public Builder abcServiceId(@Nullable final Integer abcServiceId) {
            this.abcServiceId = abcServiceId;
            return this;
        }

        @NotNull
        public Builder description(@NotNull final String description) {
            this.description = description;
            return this;
        }

        @NotNull
        public Builder parent(@Nullable final Project parent) {
            return parent(parent, false);
        }

        @NotNull
        @Deprecated
        public Builder parent(@Nullable final Project parent, final boolean immutable) {
            this.parent = parent;
            this.immutable = immutable;
            return this;
        }

        @NotNull
        public Builder subprojects(@NotNull final Collection<Project> subprojects) {
            this.subprojects.addAll(subprojects);
            return this;
        }

        @NotNull
        public Builder removed(final boolean removed) {
            this.removed = removed;
            return this;
        }

        @NotNull
        public Builder syncedWithAbc(final boolean syncedWithAbc) {
            this.syncedWithAbc = syncedWithAbc;
            return this;
        }

        @NotNull
        public Builder mailList(final String mailList) {
            this.mailList = mailList;
            return this;
        }

        @NotNull
        public Builder valueStreamAbcServiceId(final Long valueStreamId) {
            this.valueStreamAbcServiceId = valueStreamId;
            return this;
        }

        @NotNull
        @Override
        public Project build() {
            return new Project(this);
        }
    }

    interface ProjectSerializationContext {

        ProjectSerializationContext DEFAULT = new ProjectSerializationContext() {
            @Override
            @NotNull
            public Set<Person> getResponsibles(final Project project) {
                return Hierarchy.get().getProjectReader().getLinkedResponsibles(project);
            }

            @Override
            public Set<Person> getMembers(final Project project) {
                return Hierarchy.get().getProjectReader().getLinkedMembers(project);
            }

            @Override
            @NotNull
            public Set<YaGroup> getGroups(final Project project) {
                return Hierarchy.get().getProjectReader().getLinkedMemberGroups(project);
            }
        };

        @NotNull
        Collection<Person> getResponsibles(final Project project);

        @NotNull
        Collection<Person> getMembers(final Project project);

        @NotNull
        Collection<YaGroup> getGroups(final Project project);
    }

    public static class BulkProjectSerializationContext implements ProjectSerializationContext {
        @NotNull
        private final Map<Project, Set<Person>> responsibles;

        @NotNull
        private final Map<Project, Set<Person>> members;

        @NotNull
        private final Map<Project, Set<YaGroup>> groups;

        public BulkProjectSerializationContext(final Collection<Project> projects) {
            responsibles = Hierarchy.get().getProjectReader().getLinkedResponsibles(projects);
            members = Hierarchy.get().getProjectReader().getLinkedMembers(projects);
            groups = Hierarchy.get().getProjectReader().getLinkedMemberGroups(projects);
        }

        @NotNull
        @Override
        public Collection<Person> getResponsibles(final Project project) {
            return responsibles.get(project);
        }

        @NotNull
        @Override
        public Collection<Person> getMembers(final Project project) {
            return members.get(project);
        }

        @NotNull
        @Override
        public Collection<YaGroup> getGroups(final Project project) {
            return groups.get(project);
        }
    }
}
