/*
 *  Copyright (c) 2025 John Ahlroos / Devsoap
 *
 *  Creative Commons Attribution-NoDerivatives 4.0 International Public License
 *
 *  By exercising the Licensed Rights (defined below), You accept and agree
 *  to be bound by the terms and conditions of this Creative Commons
 *  Attribution-NoDerivatives 4.0 International Public License ("Public
 *  License"). To the extent this Public License may be interpreted as a
 *  contract, You are granted the Licensed Rights in consideration of Your
 *  acceptance of these terms and conditions, and the Licensor grants You
 *  such rights in consideration of benefits the Licensor receives from
 *  making the Licensed Material available under these terms and
 *  conditions.
 *
 *  Refer to LICENSE file or https://creativecommons.org/licenses/by-nd/4.0 for full license.
 *
 */

package com.devsoap.tincheck.countries;

import com.devsoap.tincheck.tin.TinCountry;
import io.micronaut.core.util.StringUtils;
import jakarta.inject.Singleton;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;

@Getter
@Singleton
@RequiredArgsConstructor
@ToString(of = {"countryCode", "countryName"})
public class Country_NO implements TinCountry {

    private final String countryCode = "NO";
    private final String countryName = "Norway";

    private final LocalDate PERIOD1_START = LocalDate.of(1854,1,1);
    private final LocalDate PERIOD2_START = LocalDate.of(1900,1,1);
    private final LocalDate PERIOD3_START = LocalDate.of(1940,1,1);
    private final LocalDate PERIOD4_START = LocalDate.of(2000,1,1);

    private final LocalDate PERIOD1_END = LocalDate.of(1899,12,31);
    private final LocalDate PERIOD2_END = LocalDate.of(1999,12,31);
    private final LocalDate PERIOD3_END = LocalDate.of(1999,12,31);
    private final LocalDate PERIOD4_END = LocalDate.of(2039,12,31);

    private static final int[] DIGIT1_WEIGHTS = {3,7,6,1,8,9,4,5,2};
    private static final int[] DIGIT2_WEIGHTS = {5,4,3,2,7,6,5,4,3,2};

    @Override
    public String generate(LocalDate yearOfBirth, Gender gender, Random randomGenerator) {
        var digits1 = String.format("%02d%02d%02d", yearOfBirth.getDayOfMonth(), yearOfBirth.getMonthValue(),
                yearOfBirth.getYear() % 100);

        IntStream identityNumberStream;
        if (isBetween(yearOfBirth, PERIOD1_START, PERIOD1_END)) {
            identityNumberStream = randomGenerator.ints(500,748);
        } else if (isBetween(yearOfBirth, PERIOD2_START, PERIOD2_END)) {
            identityNumberStream = randomGenerator.ints(0,498);
        } else if (isBetween(yearOfBirth, PERIOD3_START, PERIOD3_END)) {
            identityNumberStream = randomGenerator.ints(900,998);
        } else if (isBetween(yearOfBirth, PERIOD4_START, PERIOD4_END)) {
            identityNumberStream = randomGenerator.ints(500,998);
        } else {
            throw new IllegalArgumentException("Year of birth " + yearOfBirth.getYear() + " not supported");
        }

        int identityNumber = identityNumberStream.map(n -> switch (gender) {
            case MALE -> n % 2 == 0 ? n + 1 : n;
            case FEMALE -> n % 2 == 0 ? n : n + 1;
        }).findFirst().getAsInt();

        int[] digits = extractDigits(digits1 + String.format("%03d",identityNumber));
        int checksum1 = calculateChecksum(digits, DIGIT1_WEIGHTS);

        int[] digits2 = Arrays.copyOf(digits, digits.length+1);
        digits2[digits.length] = checksum1;
        int checksum2 = calculateChecksum(digits2, DIGIT2_WEIGHTS);

        return String.format("%s %03d %d%d", digits1, identityNumber, checksum1, checksum2);
    }

    @Override
    public boolean formatCheck(String tin) {
        if (tin.length() != 11) {
            return false;
        }
        if (!StringUtils.isDigits(tin)) {
            return false;
        }
        return true;
    }

    @Override
    public boolean logicCheck(String tin) {
        int[] digits = extractDigits(tin);
        int checksum1 = calculateChecksum(Arrays.copyOfRange(digits, 0, DIGIT1_WEIGHTS.length), DIGIT1_WEIGHTS);
        if (checksum1 != digits[9]) {
            return false;
        }
        int checksum2 = calculateChecksum(Arrays.copyOfRange(digits, 0, DIGIT2_WEIGHTS.length), DIGIT2_WEIGHTS);
        if (checksum2 != digits[10]) {
            return false;
        }
        return true;
    }

    private boolean isBetween(LocalDate date, LocalDate start, LocalDate end) {
        return date.isEqual(start) || date.isEqual(end) || (date.isAfter(start) && date.isBefore(end));
    }

    private static int calculateChecksum(int[] digits, int[] weights) {
        if (digits.length != weights.length) {
            throw new IllegalArgumentException(String.format("Checksum consists of %d digits, %d given",
                    weights.length, digits.length));
        }
        var sum = IntStream.range(0, weights.length).map(i -> digits[i] * weights[i]).sum();
        var digit =  11 - (sum % 11);
        return digit < 10 ? digit : 0;
    }
}