package ru.yandex.qe.dispenser.domain.base_resources;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
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.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
public final class BaseResourceLinearRelation {

    private final List<BaseResourceLinearRelationTerm> terms;
    private final long constantTermNumerator;
    private final long constantTermDenominator;
    private final Set<ResourceAndSegmentsId> allResources;

    @JsonCreator
    public BaseResourceLinearRelation(@JsonProperty("terms") List<BaseResourceLinearRelationTerm> terms,
                                      @JsonProperty("constantTermNumerator") long constantTermNumerator,
                                      @JsonProperty("constantTermDenominator") long constantTermDenominator) {
        this.terms = terms;
        this.constantTermNumerator = constantTermNumerator;
        this.constantTermDenominator = constantTermDenominator;
        this.allResources = terms != null
                ? terms.stream().map(t -> new ResourceAndSegmentsId(t.getResourceId(), t.getSegmentIds()))
                        .collect(Collectors.toSet())
                : Set.of();
    }

    public static Builder builder() {
        return new Builder();
    }

    public static Builder builder(BaseResourceLinearRelation value) {
        return new Builder(value);
    }

    public List<BaseResourceLinearRelationTerm> getTerms() {
        return terms;
    }

    public long getConstantTermNumerator() {
        return constantTermNumerator;
    }

    public long getConstantTermDenominator() {
        return constantTermDenominator;
    }

    @JsonIgnore
    public Set<ResourceAndSegmentsId> getAllResources() {
        return allResources;
    }

    @JsonIgnore
    public boolean isLocalEvaluation() {
        return true;
    }

    public BigDecimal evaluateLocally(Map<ResourceAndSegmentsId, Long> parameters) {
        if (parameters.isEmpty() || parameters.values().stream().allMatch(amount -> amount == 0L)) {
            // All non-constant terms are zero, skip constant term application
            return BigDecimal.ZERO;
        }
        List<Fraction> termFractions = new ArrayList<>();
        terms.forEach(term -> {
            long parameterValue = parameters
                    .getOrDefault(new ResourceAndSegmentsId(term.getResourceId(), term.getSegmentIds()), 0L);
            if (parameterValue == 0L || term.getNumerator() == 0L) {
                return;
            }
            termFractions.add(new Fraction(BigInteger.valueOf(parameterValue)
                    .multiply(BigInteger.valueOf(term.getNumerator())), BigInteger.valueOf(term.getDenominator())));
        });
        if (termFractions.stream().allMatch(f -> f.getNumerator().compareTo(BigInteger.ZERO) == 0)) {
            // All non-constant terms are zero, skip constant term application
            return BigDecimal.ZERO;
        }
        termFractions.add(new Fraction(BigInteger.valueOf(constantTermNumerator),
                BigInteger.valueOf(constantTermDenominator)));
        List<Fraction> nonZeroTerms = termFractions.stream()
                .filter(f -> f.getNumerator().compareTo(BigInteger.ZERO) != 0)
                .collect(Collectors.toList());
        if (nonZeroTerms.isEmpty()) {
            return BigDecimal.ZERO;
        }
        Fraction sum = nonZeroTerms.get(0);
        if (nonZeroTerms.size() > 1) {
            for (int i = 1; i < nonZeroTerms.size(); i++) {
                Fraction term = nonZeroTerms.get(i);
                if (sum.getDenominator().compareTo(term.getDenominator()) == 0) {
                    sum = new Fraction(sum.getNumerator().add(term.getNumerator()), sum.getDenominator());
                } else {
                    BigInteger commonDenominator = sum.getDenominator().multiply(term.getDenominator());
                    BigInteger sumNumerator = sum.getNumerator().multiply(term.getDenominator());
                    BigInteger termNumerator = term.getNumerator().multiply(sum.getDenominator());
                    sum = new Fraction(sumNumerator.add(termNumerator), commonDenominator);
                }
            }
        }
        return new BigDecimal(sum.getNumerator()).divide(new BigDecimal(sum.getDenominator()), 0, RoundingMode.UP);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        BaseResourceLinearRelation that = (BaseResourceLinearRelation) o;
        return constantTermNumerator == that.constantTermNumerator &&
                constantTermDenominator == that.constantTermDenominator &&
                Objects.equals(terms, that.terms);
    }

    @Override
    public int hashCode() {
        return Objects.hash(terms, constantTermNumerator, constantTermDenominator);
    }

    @Override
    public String toString() {
        return "BaseResourceLinearRelation{" +
                "terms=" + terms +
                ", constantTermNumerator=" + constantTermNumerator +
                ", constantTermDenominator=" + constantTermDenominator +
                '}';
    }

    public static final class Builder {

        private final List<BaseResourceLinearRelationTerm> terms = new ArrayList<>();

        private Long constantTermNumerator;
        private Long constantTermDenominator;

        private Builder() {
        }

        private Builder(BaseResourceLinearRelation value) {
            this.terms.addAll(value.terms != null ? value.terms : List.of());
            this.constantTermNumerator = value.constantTermNumerator;
            this.constantTermDenominator = value.constantTermDenominator;
        }

        public Builder addTerm(BaseResourceLinearRelationTerm term) {
            this.terms.add(term);
            return this;
        }

        public Builder addTerms(Collection<? extends BaseResourceLinearRelationTerm> terms) {
            this.terms.addAll(terms);
            return this;
        }

        public Builder addTerms(BaseResourceLinearRelationTerm... terms) {
            this.terms.addAll(List.of(terms));
            return this;
        }

        public Builder constantTermNumerator(long constantTermNumerator) {
            this.constantTermNumerator = constantTermNumerator;
            return this;
        }

        public Builder constantTermDenominator(long constantTermDenominator) {
            this.constantTermDenominator = constantTermDenominator;
            return this;
        }

        public BaseResourceLinearRelation build() {
            Objects.requireNonNull(constantTermNumerator, "Constant term numerator is required");
            Objects.requireNonNull(constantTermDenominator, "Constant term denominator is required");
            return new BaseResourceLinearRelation(terms, constantTermNumerator, constantTermDenominator);
        }

    }

}
