package cz.moneta.test.dsl.util.data.generators;

import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.util.DateUtils;
import cz.moneta.test.dsl.util.TestUtils;
import cz.moneta.test.dsl.util.data.client.IdCard;
import cz.moneta.test.dsl.util.task.mvcr.MvcrTasks;
import cz.moneta.test.dsl.util.task.nasdb.NasDbTasks;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.data.Generator;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Stream;

import static java.time.temporal.ChronoUnit.YEARS;

/**
 * Generate random ID issued through the year 2018 with default validity set to 10 years (person 15 - 69 years old when issued).
 */
public class IdCardGenerator implements Generator<IdCard> {
    private static final Logger logger = LogManager.getLogger(IdCardGenerator.class);

    private static final LocalDate INITIAL_DATE = LocalDate.of(2000, 1, 3);
    private static final int IDCARD_DEFAULT_VALIDITY_YEARS = 10;

    private static final Map<Range<Integer>, Coefficients> COEFFICIENTS = new HashMap<Range<Integer>, Coefficients>() {{
        put(Range.between(209127454, 209417120), Coefficients.of(3.2752797189192423E-19, 0, -0.04272685301755383, 5946347.60536327));
        put(Range.between(209417121, 209746461), Coefficients.of(0, -2.822014571389389E-11, 0.012099543121958684, -1289576.6013491666));
        put(Range.between(209746462, 210131118), Coefficients.of(0, 0, 2.3645814848137332E-4, -42841.37467391353));
        put(Range.between(210131119, 210462589), Coefficients.of(7.57301930917743E-19, -2.38294971484551E-10, 0, 3502262.90406278));
    }};

    private static final Integer MIN_NUMBER = COEFFICIENTS.keySet().stream()
            .map(Range::getMinimum)
            .min(Comparator.naturalOrder())
            .orElseThrow(() -> new IllegalStateException("Could not determine minimum ID Card number"));

    private static final Integer MAX_NUMBER = COEFFICIENTS.keySet().stream()
            .map(Range::getMaximum)
            .max(Comparator.naturalOrder())
            .orElseThrow(() -> new IllegalStateException("Could not determine maximum ID Card number"));

    @Override
    public IdCard generate(StoreAccessor harness, Object... params) {
        Optional<LocalDate> birthDate = getBirthDate(params);
        MvcrTasks mvcrTasks = new MvcrTasks((Harness) harness);
        NasDbTasks nasDbTasks = new NasDbTasks(harness);
        Pair<Integer, LocalDate> id = Stream.generate(this::getIdNumberAndIssueDate)
                .filter(p -> DateUtils.isWorkingDay(p.getRight()))
                .filter(p -> birthDate.map(p.getRight()::isAfter).orElse(true))
                .limit(10)
                .filter(p -> mvcrTasks.checkIdCardValidityWso2(p.getLeft().toString()))
                .filter(p -> !nasDbTasks.checkPresentIdOnBlacklist(p.getLeft().toString()))
                .findFirst()
                .orElseThrow(() -> new HarnessException("Failed to generate appropriate ID card after 10 attempts"));
        int validityYears = resolveValidity(birthDate);
        logger.log(Level.INFO, "Generated ID card number {} with expiration {}.", id.getLeft(), id.getRight().plusYears(validityYears));
        return new IdCard(String.valueOf(id.getLeft()), id.getRight(), id.getRight().plusYears(validityYears));
    }

    private int resolveValidity(Optional<LocalDate> birthDate) {
        return Optional.ofNullable(birthDate)
                .map(d -> YEARS.between(d.get(), LocalDate.now())) // Count age
                .map(age -> {
                    if (age < 15) {
                        return 5;
                    } else if (age >= 70) {
                        return 35;
                    } else {
                        return IDCARD_DEFAULT_VALIDITY_YEARS;
                    }
                })
                .orElse(IDCARD_DEFAULT_VALIDITY_YEARS);
    }

    private Optional<LocalDate> getBirthDate(Object[] params) {
        return Optional.ofNullable(params)
                .filter(p -> p.length == 1)
                .map(p -> p[0])
                .map(String::valueOf)
                .map(TestUtils::parseBirthDate)
                .map(LocalDate.class::cast);
    }

    private Pair<Integer, LocalDate> getIdNumberAndIssueDate() {
        int cardNumber = ThreadLocalRandom.current().nextInt(MIN_NUMBER, MAX_NUMBER + 1);
        LocalDate date = COEFFICIENTS.entrySet().stream()
                .filter(e -> e.getKey().contains(cardNumber))
                .map(Map.Entry::getValue)
                .map(cs -> getNumberOfDays(cardNumber, cs))
                .map(INITIAL_DATE::plusDays)
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("Failed to generate ID card issue date"));
        return Pair.of(cardNumber, date);
    }

    private int getNumberOfDays(int cardNumber, Coefficients coefs) {
        BigDecimal number = BigDecimal.valueOf(cardNumber);
        return coefs.getC4()
                .add(coefs.getC3().multiply(number))
                .add(coefs.getC2().multiply(number.pow(2)))
                .add(coefs.getC1().multiply(number.pow(3)))
                .intValue();
    }

    private static class Coefficients {

        private final BigDecimal c1;
        private final BigDecimal c2;
        private final BigDecimal c3;
        private final BigDecimal c4;

        private Coefficients(double c1, double c2, double c3, double c4) {
            this.c1 = BigDecimal.valueOf(c1);
            this.c2 = BigDecimal.valueOf(c2);
            this.c3 = BigDecimal.valueOf(c3);
            this.c4 = BigDecimal.valueOf(c4);
        }

        private static Coefficients of(double c1, double c2, double c3, double c4) {
            return new Coefficients(c1, c2, c3, c4);
        }

        private BigDecimal getC1() {
            return c1;
        }

        private BigDecimal getC2() {
            return c2;
        }

        private BigDecimal getC3() {
            return c3;
        }

        private BigDecimal getC4() {
            return c4;
        }

    }

}