package eu.dnetlib.validator2.validation.guideline;

import eu.dnetlib.validator2.engine.*;
import eu.dnetlib.validator2.engine.builtins.StandardRuleDiagnostics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class GuidelineEvaluation {

    private static final Logger logger = LoggerFactory.getLogger(GuidelineEvaluation.class);


    // TODO: Report all rule diagnostics to System out/err (we should remove this or allow clients to disable it)
    private static final RuleDiagnostics<Document, Rule<Document>> OUT = Helper.Diagnostics.systemOut();
    private static final RuleDiagnostics<Document, Rule<Document>> ERR = Helper.Diagnostics.systemErr();

    private final String subjectId;
    private final Document doc;
    private final int weight;
    private final List<String> warnings = new ArrayList<>();
    private final List<String> errors = new ArrayList<>();
    private Map<String, RequirementLevel> ruleIdToRequirementLevel = new HashMap<>();
    private Map<String, NodeList> ruleIdToNodeList = new HashMap<>();
    private final Diagnostics diagnostics = new Diagnostics();
    private final Reporter<Document, SyntheticRule<Document>> reporter = new Reporter<>(diagnostics);

    GuidelineEvaluation(String subjectId, Document doc, int weight) {
        this.subjectId = subjectId;
        this.doc = doc;
        this.weight = weight;
    }

    Guideline.Result evaluate(CompilationResult result) {

        ruleIdToRequirementLevel.putAll(result.ruleIdToRequirementLevel);

        List<SyntheticRule<Document>> rules = new ArrayList<>();
        rules.add(result.rootNodeRule);
        rules.addAll(result.nodeRules);

        logger.debug("Evaluating " + rules);

        for ( SyntheticRule<Document> rule: rules ) {

            RuleEngine.applyAndReport(rule, doc, reporter);

            Status status = diagnostics.getLastReportedStatus();
            if ( status == Status.ERROR ) {
                // fail fast in case of errors (no reason to proceed, since results may be rubbish)
                return StandardResult.forError(diagnostics.getLastReportedError().getMessage());
            }

            String id = rule.getContext().getIdProperty().getValue();
            RequirementLevel requirementLevel = getRequirementLevelOf(id);

            if ( status == Status.SUCCESS
                    && (requirementLevel == RequirementLevel.NOT_APPLICABLE) ) {
                // Report the non-applicability of a rule as a warning
                // The check for both status and non-applicable requirement level is redundant
                // (non-applicable rules always succeed), yet it is more clear, hopefully.
                logger.warn("Non-applicable: " + rule);
                warnings.add(synthesizeFailureMessage(rule));
            }
            else if ( status == Status.FAILURE )
            {
                if ( requirementLevel == RequirementLevel.MANDATORY
                    || requirementLevel == RequirementLevel.MANDATORY_IF_APPLICABLE )
                {
                    // A mandatory rule has failed, yet we don't know whether we should report is as such.

                    // Let's check the parent of the rule
                    if ( rule.parentRule() == null ) {
                        // This is the root rule failing!
                        // Fail fast here, too (don't waste resources to evaluate other rules).
                        // We will "enable" it, if it is requested.
                        logger.error("Fail fast for root failure: " + rule);
                        errors.add(synthesizeFailureMessage(rule));
                        return StandardResult.forFailure(warnings, errors);
                    } else {
                        // The current rule has a parent/ancestor which:
                        // (a) is non-mandatory or
                        // (b) it was successful.
                        // Thus, here we need only to warn and not to err, allowing the evaluation loop to proceed.
                        if ( requirementLevel == RequirementLevel.MANDATORY )
                            logger.warn("Mandatory failure: " + rule);
                        else
                            logger.warn("Mandatory_If_Applicable failure: " + rule);

                        warnings.add(synthesizeFailureMessage(rule));
                    }
                } else {
                    // This is a warning: a non-mandatory rule has failed.
                    // Note that MA rules are treated as non-mandatory.
                    // We let the evaluation loop proceed.
                    if ( requirementLevel == RequirementLevel.RECOMMENDED )
                        logger.warn("Recommended failure: " + rule);
                    else if ( requirementLevel == RequirementLevel.OPTIONAL )
                        logger.warn("Optional failure: " + rule);
                    else    // This should never happen, but better catch this case, than leave it be.
                        logger.error("UNKNOWN failure: " + rule);   // It is not "Mandatory", in any form, though.

                    warnings.add(synthesizeFailureMessage(rule));
                }
            }
            // Else it is just "SUCCESS" for an applicable rule, which means there is no error or warning to create.
        }

        int returnedWeight = weight;
        if ( rules.size() == warnings.size() ) {
            // TODO: I think this is the desired behavior, but we need to get confirmation for this.
            // All rules have failed, but with warnings (thus being either optional or recommended).
            // This indicates a success with 0 weight.
            returnedWeight = 0;
        }
        return StandardResult.forSuccess(returnedWeight, warnings);
    }

    private String synthesizeFailureMessage(Rule<Document> rule) {
        return subjectId + ": rule " + Helper.stringify(rule) + " has failed";
    }

    private String synthesizeNotApplicableMessage(Rule<Document> rule) {
        return subjectId + ": rule " + Helper.stringify(rule) + " is not applicable";
    }


    void setNodesOf(String ruleId, NodeList nodes) {
        ruleIdToNodeList.put(ruleId, nodes);
    }

    NodeList getNodesOf(String ruleId) {
        return ruleIdToNodeList.get(ruleId);
    }

    void setRequirementLevelOf(String ruleId, RequirementLevel requirementLevel) {
        ruleIdToRequirementLevel.put(ruleId, requirementLevel);
    }

    RequirementLevel getRequirementLevelOf(String ruleId) {
        return ruleIdToRequirementLevel.get(ruleId);
    }

    @Override
    public String toString() {
        return "GuidelineEvaluation{" +
                "subjectId='" + subjectId + '\'' +
                ", doc=" + doc +
                ", weight=" + weight +
                ", warnings=" + warnings +
                ", errors=" + errors +
                ", ruleIdToRequirementLevel=" + ruleIdToRequirementLevel +
                ", ruleIdToNodeList=" + ruleIdToNodeList +
                ", diagnostics=" + diagnostics +
                ", reporter=" + reporter +
                '}';
    }


    private static final class Diagnostics extends StandardRuleDiagnostics<Document, SyntheticRule<Document>> {

        private final Map<String, Status> statusByRuleId = new HashMap<>();

        @Override
        public void success(SyntheticRule<Document> rule, Document document) {
            OUT.success(rule, document);
            super.success(rule, document);
            statusByRuleId.put(rule.getContext().getIdProperty().getValue(), Status.SUCCESS);
        }

        @Override
        public void failure(SyntheticRule<Document> rule, Document document) {
            OUT.failure(rule, document);
            super.failure(rule, document);
            statusByRuleId.put(rule.getContext().getIdProperty().getValue(), Status.FAILURE);
        }

        @Override
        public void error(SyntheticRule<Document> rule, Document document, Throwable err) {
            ERR.error(rule, document, err);
            super.error(rule, document, err);
            statusByRuleId.put(rule.getContext().getIdProperty().getValue(), Status.ERROR);
        }

        private Status statusFor(String ruleId) {
            return statusByRuleId.get(ruleId);
        }

        @Override
        public String toString() {
            return "Diagnostics{" +
                    "statusByRuleId=" + statusByRuleId +
                    '}';
        }
    }
}
