package me.teridax.jcash.banking.management;

import me.teridax.jcash.banking.Bank;
import me.teridax.jcash.banking.accounts.Account;
import me.teridax.jcash.banking.accounts.Owner;
import me.teridax.jcash.decode.StringDecoder;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

import static me.teridax.jcash.Logging.LOGGER;

/**
 * Management system for banks and all accounts provided by these banks and their respective account owners.
 * This class serves a read only database which can only modify runtime data without any respect to CRUD or the ACID
 * principles.
 */
public final class BankingManagementSystem {

    /**
     * Separator used to separate columns of CSV files
     */
    private static final String SEPARATOR = ";";

    /**
     * A set of banks
     */
    private final Set<Bank> banks;

    private BankingManagementSystem() {
        this.banks = new HashSet<>();
    }

    /**
     * Utility method for retrieving the tail of a string array.
     * This method return all items which follow the n-th index denoted by index.
     *
     * @param array the array to take the tail of
     * @param index the amount trailing indices to skip
     * @return an array containing the last elements of the supplied array
     */
    private static String[] tail(String[] array, int index) {
        return Arrays.stream(array).skip(index).toArray(String[]::new);
    }

    /**
     * Loads an instance of this class from a csv file.
     * The csv file must comply to the formats of {@link StringDecoder} and must contain
     * the following columns in this exact order:
     * <ol>
     *     <li>Bank name</li>
     *     <li>Bank BLZ</li>
     *     <li>Account IBAN</li>
     *     <li>Account PIN</li>
     *     <li>Account Balance</li>
     *     <li>Account type</li>
     *     <li>Account Interest rate</li>
     *     <li>Account Overdraft</li>
     *     <li>Owner uuid</li>
     *     <li>Owner family name</li>
     *     <li>Owner name</li>
     *     <li>Owner street</li>
     *     <li>Owner plz</li>
     *     <li>Owner city</li>
     * </ol>
     * The file can contain a header line which gets skipped when reading.
     * Note that the first line is always skipped and the name of every column is not checked in any way.
     *
     * @param file the file to parse
     * @return a valid BMS
     * @throws IllegalArgumentException if the file cannot be read or the containing data is invalid
     */
    public static BankingManagementSystem loadFromCsv(Path file) throws IllegalArgumentException {
        LOGGER.fine("parsing banking management system from file: " + Objects.toString(file, "null"));
        try {
            var bms = new BankingManagementSystem();
            var content = Files.readString(file);

            // read line by line
            // and skip the first line
            content.lines().skip(1).forEach(line -> {

                LOGGER.finest("splitting lines by separator: " + SEPARATOR);
                // split the line into columns
                var columns = line.split(SEPARATOR);

                // one line must contain exactly 14 columns
                if (columns.length != 14)
                    throw new IllegalArgumentException("invalid column count: " + columns.length);

                LOGGER.finer("reading members from line: " + line);
                // read basic fields
                var owner = Owner.fromColumns(tail(columns, 8));
                var account = Account.fromColumns(tail(columns, 2));

                var blz = Bank.validateBlz(columns[1]);
                var name = StringDecoder.decodeName(columns[0]);
                var bankOfLine = new Bank(blz, name);

                // add banks to bms

                var bankOfSet = bms.banks.stream().filter(b -> b.equals(bankOfLine)).findFirst();
                if (bankOfSet.isPresent()) {
                    LOGGER.fine("bank from current line is already present in management system");
                    bankOfSet.get().addAccount(owner, account);
                } else {
                    LOGGER.fine("bank from current line is new for management system");
                    bankOfLine.addAccount(owner, account);
                    bms.banks.add(bankOfLine);
                }
            });

            return bms;

        } catch (IOException e) {
            LOGGER.severe("Could not read file: " + file + " due to: " + e.getMessage());
            throw new IllegalArgumentException("Could not read file " + file, e);
        } catch (IllegalArgumentException | NullPointerException e) {
            LOGGER.severe("Could not parse file: " + file + " due to: " + e.getMessage());
            throw new IllegalArgumentException("Could not parse file " + file, e);
        }
    }

    /**
     * Return a bank with the given blz.
     *
     * @param blz the blz to search bank of
     * @return the bank with this blz or none
     */
    public Optional<Bank> getBank(String blz) {
        return this.banks.stream().filter(b -> b.getBlz().equals(blz)).findFirst();
    }
}
