package encapsulation;

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

class Person {
  private String[] name = {""};
  private String email = "";
  private Date birthday;
  private char gender = '\0';
  private String ssn;

  public Date getBirthday() {
    return this.birthday;
  }

  public String getEmail() {
    return this.email;
  }

  public char getGender() {
    return this.gender;
  }

  public String getName() {
    return String.join(" ", this.name);
  }

  public String getSSN() {
    return this.ssn;
  }

  public void setName(String name) throws IllegalArgumentException {
    final String nameFormat = "[A-ZÆØÅa-zæøå]{2,}";
    final String fullNameFormat = String.format("%s %s", nameFormat, nameFormat);

    if (!name.matches(fullNameFormat))
      throw new IllegalArgumentException("Couldn't parse name");

    this.name = name.split(" ");
  }

  private boolean checkEmail(String email) {
    final Set<String> cTLDs = Set.of("ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as", "at", "au", "aw",
        "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bl", "bm", "bn", "bo", "bq", "br", "bs",
        "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr",
        "cu", "cv", "cw", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es",
        "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn",
        "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "im",
        "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr",
        "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me",
        "mf", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my",
        "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg",
        "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb",
        "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st", "sv", "sx", "sy",
        "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv", "tw", "tz", "ua",
        "ug", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm",
        "zw");

    String[] emailParts = email.split("[.@]");

    if (emailParts.length != 4) return false;
    return Stream.of(
      emailParts[0].equalsIgnoreCase(this.name[0]),
      emailParts[1].equalsIgnoreCase(this.name[1]),
      emailParts[2].matches("[A-Za-z0-9]+"), // "\w" includes '_', so it's no good here.
      cTLDs.contains(emailParts[3])
    ).allMatch(b -> b);
  }

  public void setEmail(String email) throws IllegalArgumentException {
    if (!checkEmail(email))
      throw new IllegalArgumentException("Couldn't parse email. Is the name correct?");

    this.email = email;
  }

  public void setBirthday(Date birthday) throws IllegalArgumentException {
    Date now = new Date(System.currentTimeMillis());
    if (!birthday.before(now))
      throw new IllegalArgumentException("This date is invalid. Please set a date in the past.");	

    this.birthday = birthday;
  }

  public void setGender(char gender) throws IllegalArgumentException {
    final Set<Character> legalGenders = Set.of('M', 'F', '\0');

    if (!legalGenders.contains(gender))
      throw new IllegalArgumentException("Couldn't parse gender.");

    this.gender = gender;
  }

  /**
   * Emphasize a range of characters within a String
   * @param s     The string to modify
   * @param start The start index of the range
   * @param end   The end index of the range
   * @return      The modified string with an emphazised range
   */
  private static String emphasizeStringRange(String s, int start, int end) 
    throws IndexOutOfBoundsException {
      return s.substring(0, start)
           + " <"
           + s.substring(start, end)
           + "> "
           + s.substring(end);
  }

  /**
   * Make sure that the birthdate of the SSN is equal to the
   * birthday of the person
   * @param ssn The SSN to validate
   */
  private void checkSSNBirthday(String ssn) throws IllegalArgumentException {

    // Calendar birthdate = Calendar.getInstance();

    try {
      Date birthday =
        DateFormat
          .getDateInstance(DateFormat.SHORT)
          .parse(
            ssn.substring(0, 2) + "/" +
            ssn.substring(2, 4) + "/" +
            ssn.substring(4, 6)
            );

      if (!birthday.equals(this.birthday))
        throw new IllegalArgumentException(
          "The SSN birthday does not match this persons birthday"
        );

    } catch (ParseException e) {
      throw new IllegalArgumentException(
        "Could not parse SSN date: " + emphasizeStringRange(ssn, 0, 6)
      );
    }
  }

  /**
   * Make sure that the SSN is valid based on the persons gender
   * @param ssn The ssn to validate
   */
  private void checkSSNMaleFemaleDigit(String ssn) throws IllegalArgumentException {
    int maleOrFemaleDigit = Integer.parseInt(ssn.substring(8, 9));
    boolean isMale = maleOrFemaleDigit % 2 == 1;
    boolean isGenderCorrect = 
      (this.gender == 'M' && isMale) || (this.gender == 'F' && !isMale);

    if (!isGenderCorrect)
      throw new IllegalArgumentException(
        "The digit "
        + emphasizeStringRange(ssn, 8, 9)
        + " has to be different because the person is a "
        + (isMale ? "male" : "female")
      );
  }

  /**
   * Check that the control ciphers are valid.
   * @param ssn The SSN to validate
   */
  private void checkSSNControlCiphers(String ssn) throws IllegalArgumentException {

    final List<Integer> f = List.of(3, 7, 6, 1, 8, 9, 4, 5, 2);
    final List<Integer> g = List.of(5, 4, 3, 2, 7, 6, 5, 4, 3, 2);

    final BiFunction<List<Integer>, List<Integer>, Integer> processDigits =
      (ds, cs) ->
        IntStream
          .range(0, ds.size())
          .map(i -> ds.get(i) * cs.get(i))
          .sum();

    List<Integer> digits = 
      ssn
        .substring(0, 9)
        .chars()
        .map(c -> c - '0') // remove offset from 0x00 to '0' 
        .boxed()           // IntStream -> Stream<Integer>
        .collect(Collectors.toList());

    Integer k1 = 11 - processDigits.apply(digits, f) % 11;
    digits.add(k1);
    Integer k2 = 11 - processDigits.apply(digits, g) % 11;

    boolean isControlCiphersCorrect = 
      (k1.toString() + k2.toString())
      .equals(ssn.substring(9));
    
    if (!isControlCiphersCorrect)
      throw new IllegalArgumentException(
        String.format(
          """
          Control ciphers do not match the given input: %s
          Expected: %s""",
          emphasizeStringRange(ssn, 9, 11),
          (k1 + " " + k2)
        )
      );
  }

  /**
   * Validate an SSN based on the persons info.
   * {@link #setBirthday(Date) setBirthday()} and {@link #setGender(char) setGender()}
   * must have been run in order for this function to work.
   * @param ssn The SSN to validate
   */
  private void checkSSN(String ssn) throws IllegalArgumentException {
    if (!ssn.matches("\\d{11}"))
      throw new IllegalArgumentException("The amount of digits does not match the expected amount");
    checkSSNBirthday(ssn);
    checkSSNMaleFemaleDigit(ssn);
    checkSSNControlCiphers(ssn);
  }

  public void setSSN(String ssn) throws IllegalArgumentException {
    checkSSN(ssn);
    this.ssn = ssn;
  }
}
