package ru.yandex.qe.dispenser.ws.quota.request;

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.inject.Inject;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Table;
import com.opencsv.CSVWriter;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Component;

import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.dao.bot.BotConfiguration;
import ru.yandex.qe.dispenser.domain.dao.bot.CompleteBotConfiguration;
import ru.yandex.qe.dispenser.domain.dao.bot.configuration.BotConfigurationDao;
import ru.yandex.qe.dispenser.domain.dao.project.ProjectReader;
import ru.yandex.qe.dispenser.domain.dao.quota.request.QuotaChangeRequestDao;
import ru.yandex.qe.dispenser.domain.dao.quota.request.QuotaChangeRequestReader;
import ru.yandex.qe.dispenser.domain.dao.quota.request.RequestPreOrderAggregationEntry;
import ru.yandex.qe.dispenser.domain.dao.service.ServiceReader;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.hierarchy.HierarchySupplier;
import ru.yandex.qe.dispenser.ws.bot.BotResourceType;

@Component
public class RequestPreOrderAggregationReport {

    public static final int MAX_PROJECT_LEVEL = 4;
    private static final NumberFormat AMOUNT_FORMAT = new DecimalFormat("#0.##");

    private final QuotaChangeRequestDao requestDao;
    private final HierarchySupplier hierarchySupplier;
    private final BotConfigurationDao botConfigurationDao;

    @Inject
    public RequestPreOrderAggregationReport(final QuotaChangeRequestDao requestDao, final HierarchySupplier hierarchySupplier,
                                            final BotConfigurationDao botConfigurationDao) {
        this.requestDao = requestDao;
        this.hierarchySupplier = hierarchySupplier;
        this.botConfigurationDao = botConfigurationDao;
    }

    private List<Project> getPathFromRoot(final Project project) {
        final List<Project> pathToRoot = project.getPathFromRoot();
        if (pathToRoot.size() <= MAX_PROJECT_LEVEL + 1) {
            return pathToRoot;
        }
        final ArrayList<Project> projects = new ArrayList<>(pathToRoot.subList(0, MAX_PROJECT_LEVEL));
        projects.add(pathToRoot.get(pathToRoot.size() - 1));
        return projects;
    }

    public void writeReport(final CSVWriter csvWriter, final QuotaChangeRequestReader.QuotaChangeRequestFilter filter, final Pivot pivot) {

        final Hierarchy hierarchy = hierarchySupplier.get();
        final ProjectReader projectReader = hierarchy.getProjectReader();
        final ServiceReader serviceReader = hierarchy.getServiceReader();

        final List<RequestPreOrderAggregationEntry> aggregationEntries = requestDao.readPreOrderAggregation(filter);

        final Set<Long> serverIds = aggregationEntries.stream().map(RequestPreOrderAggregationEntry::getServerId).collect(Collectors.toSet());
        final Map<Long, Boolean> hasConfigurationGpuById =
                botConfigurationDao.readAll(serverIds).stream()
                        .filter(BotConfiguration::isWithComponents)
                        .map(x -> (CompleteBotConfiguration) x)
                        .collect(Collectors.toMap(BotConfiguration::getId, x -> x.getConfiguration().getConfigurationComponents().stream().anyMatch(BotResourceType.GPU::is)));

        final Table<Long, Pair<Long, Boolean>, Double> sumByProjectAndServerId = HashBasedTable.create();

        final SetMultimap<Long, Project> projectByParentId = HashMultimap.create();

        for (final RequestPreOrderAggregationEntry entry : aggregationEntries) {
            final Pair<Long, Boolean> serviceServerId = Pair.of(entry.getServiceId(), hasConfigurationGpuById.getOrDefault(entry.getServerId(), false));
            final Project project = projectReader.read(entry.getProjectId());
            final List<Project> projects = getPathFromRoot(project);
            for (int i = 0; i < projects.size(); i++) {
                final Project pathProject = projects.get(i);
                Double sum = sumByProjectAndServerId.get(pathProject.getId(), serviceServerId);
                if (sum == null) {
                    sum = 0.0;
                }
                sumByProjectAndServerId.put(pathProject.getId(), serviceServerId, sum + pivot.getValue(entry));
                if (i > 0) {
                    projectByParentId.put(projects.get(i - 1).getId(), pathProject);
                }
            }
        }

        final ArrayList<Pair<Object, String>> columns = new ArrayList<>();

        for (int i = 1; i <= MAX_PROJECT_LEVEL; i++) {
            columns.add(Pair.of("HEAD_" + i, "Service #" + i));
        }
        columns.add(Pair.of("TOTAL", "Total"));

        final List<Pair<Long, Boolean>> serviceServersColunms
                = new ArrayList<>(sumByProjectAndServerId.columnKeySet());
        Collections.sort(serviceServersColunms);

        for (final Pair<Long, Boolean> serviceServersColunm : serviceServersColunms) {
            columns.add(Pair.of(serviceServersColunm, serviceReader.read(serviceServersColunm.getKey()).getName() + (serviceServersColunm.getValue() ? " GPU" : "")));
        }
        final List<Map<Object, Object>> rows = new ArrayList<>();
        rows.add(columns.stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue)));

        final Project root = projectReader.getRoot();
        rows.addAll(process(projectByParentId, sumByProjectAndServerId, root.getId(), 1));

        final Map<Object, Object> totalRow = getRow(root, sumByProjectAndServerId.row(root.getId()), 1);
        totalRow.put("HEAD_1", "Total");
        rows.add(totalRow);

        final String[] params = new String[columns.size()];
        for (final Map<Object, Object> row : rows) {
            int i = 0;
            for (final Pair<Object, String> column : columns) {
                final Object value = row.get(column.getKey());
                params[i] = value == null ? "" : String.valueOf(value);
                i += 1;
            }

            csvWriter.writeNext(params);
        }

    }

    private List<Map<Object, Object>> process(final SetMultimap<Long, Project> projectByParentId, final Table<Long, Pair<Long, Boolean>, Double> sumByProjectAndServerId, final long parentId, final int level) {
        final ArrayList<Project> projects = new ArrayList<>(projectByParentId.get(parentId));
        if (projects.isEmpty()) {
            return Collections.emptyList();
        }
        Collections.sort(projects);
        final ArrayList<Map<Object, Object>> result = new ArrayList<>();
        for (final Project project : projects) {
            result.add(getRow(project, sumByProjectAndServerId.row(project.getId()), level));
            result.addAll(process(projectByParentId, sumByProjectAndServerId, project.getId(), level + 1));
        }

        return result;
    }

    private Map<Object, Object> getRow(final Project project, final Map<Pair<Long, Boolean>, Double> sumsByServiceServer, final int level) {
        final HashMap<Object, Object> row = new HashMap<>();

        row.put("HEAD_" + level, project.getName());

        double total = 0.0;
        for (final Pair<Long, Boolean> serviceServer : sumsByServiceServer.keySet()) {
            final Double value = sumsByServiceServer.get(serviceServer);
            total += value;
            row.put(serviceServer, AMOUNT_FORMAT.format(value));
        }

        row.put("TOTAL", AMOUNT_FORMAT.format(total));

        return row;
    }

    public enum Pivot {
        COST(RequestPreOrderAggregationEntry::getTotalCost),
        SERVERS(RequestPreOrderAggregationEntry::getTotalServersQuantity);

        private final Function<RequestPreOrderAggregationEntry, Double> getter;

        Pivot(final Function<RequestPreOrderAggregationEntry, Double> getter) {
            this.getter = getter;
        }

        public Double getValue(final RequestPreOrderAggregationEntry entry) {
            return getter.apply(entry);
        }
    }
}
