// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.configserver.noderepository;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.reports.BaseReport;

import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

import static com.yahoo.yolean.Exceptions.uncheck;

/**
 * API of node reports within node-admin.
 *
 * @author hakonhall
 */
public class NodeReports {
    private static final ObjectMapper mapper = new ObjectMapper();

    private final Map<String, JsonNode> reports = new TreeMap<>();

    public NodeReports() { }

    public NodeReports(NodeReports reports) {
        this.reports.putAll(reports.reports);
    }

    private NodeReports(Map<String, JsonNode> reports) {
        this.reports.putAll(Objects.requireNonNull(reports));
    }

    public static NodeReports fromMap(Map<String, JsonNode> reports) {
        return new NodeReports(reports);
    }

    public void setReport(String reportId, JsonNode jsonNode) {
        reports.put(reportId, jsonNode);
    }

    public boolean hasReport(String reportId) { return reports.containsKey(reportId); }

    public <T> Optional<T> getReport(String reportId, Class<T> jacksonClass) {
        return Optional.ofNullable(reports.get(reportId)).map(r -> uncheck(() -> mapper.treeToValue(r, jacksonClass)));
    }

    /** Gets all reports of the given types and deserialize with the given jacksonClass. */
    public <T> TreeMap<String, T> getReports(Class<T> jacksonClass, BaseReport.Type... types) {
        Set<BaseReport.Type> typeSet = Set.of(types);

        return reports.entrySet().stream()
                .filter(entry -> {
                    JsonNode reportType = entry.getValue().findValue(BaseReport.TYPE_FIELD);
                    if (reportType == null || !reportType.isTextual()) return false;
                    Optional<BaseReport.Type> type = BaseReport.Type.deserialize(reportType.asText());
                    return type.map(typeSet::contains).orElse(false);
                })
                .collect(Collectors.toMap(
                        entry -> entry.getKey(),
                        entry -> uncheck(() -> mapper.treeToValue(entry.getValue(), jacksonClass)),
                        (x,y) -> x, // resolves key collisions - cannot happen.
                        TreeMap::new
                ));
    }

    public void removeReport(String reportId) {
        if (reports.containsKey(reportId)) {
            reports.put(reportId, null);
        }
    }

    public Map<String, JsonNode> getRawMap() {
        return new TreeMap<>(reports);
    }

    /** Set or overwrite the reports of {@code this} that are found in {@code override}. */
    public void updateFromRawMap(Map<String, JsonNode> override) {
        override.forEach((reportId, jsonNode) -> {
            if (jsonNode != null) {
                reports.put(reportId, jsonNode);
            }
        });
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        NodeReports that = (NodeReports) o;
        return Objects.equals(reports, that.reports);
    }

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

@@ -2881,87 +2881,69 @@
 /** 
-Set or
+Apply the
  over
-w
 ri
-te the reports of %7B@code this%7D that are found in %7B@code override%7D
+de to this.  null value means removing report
 . */
@@ -3086,17 +3086,17 @@
 sonNode 
-!
+=
 = null) 
@@ -3093,24 +3093,87 @@
  == null) %7B%0A
+                reports.remove(reportId);%0A            %7D else %7B%0A
             

remove report if null value