MoneyWiseArchiveCache.java

/*
 * MoneyWise: Finance Application
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package io.github.tonywasher.joceanus.moneywise.archive;

import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataFieldId;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetBase;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetDirection;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetType;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicDataType;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicResource;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDataSet;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio.MoneyWisePortfolioList;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurity;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityHolding;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityHolding.MoneyWiseSecurityHoldingMap;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransAsset;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction.MoneyWiseTransactionList;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransCategoryClass;
import io.github.tonywasher.joceanus.moneywise.exc.MoneyWiseDataException;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataItem;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataValues;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

/**
 * Parent Cache details.
 */
public final class MoneyWiseArchiveCache {
    /**
     * DataSet.
     */
    private final MoneyWiseDataSet theData;

    /**
     * TransactionList.
     */
    private final MoneyWiseTransactionList theList;

    /**
     * The map of names to assets.
     */
    private final Map<String, Object> theNameMap;

    /**
     * The map of names->categories.
     */
    private final Map<String, MoneyWiseTransCategory> theCategoryMap;

    /**
     * The list of years.
     */
    private final List<MoneyWiseArchiveYear> theYears;

    /**
     * Are we filtering?.
     */
    private boolean enableFiltering;

    /**
     * Last Parent.
     */
    private MoneyWiseTransaction theLastParent;

    /**
     * Last Debit.
     */
    private Object theLastDebit;

    /**
     * Last Credit.
     */
    private Object theLastCredit;

    /**
     * The parent.
     */
    private MoneyWiseTransaction theParent;

    /**
     * Split Status.
     */
    private boolean isSplit;

    /**
     * Resolved Date.
     */
    private OceanusDate theDate;

    /**
     * AssetPair Id.
     */
    private MoneyWiseAssetDirection theDirection;

    /**
     * Resolved Account.
     */
    private MoneyWiseTransAsset theAccount;

    /**
     * Resolved Partner.
     */
    private MoneyWiseTransAsset thePartner;

    /**
     * Resolved Transaction Category.
     */
    private MoneyWiseTransCategory theCategory;

    /**
     * Resolved Portfolio.
     */
    private MoneyWisePortfolio thePortfolio;

    /**
     * Is the Debit reversed?
     */
    private boolean isDebitReversed;

    /**
     * The last event.
     */
    private OceanusDate theLastEvent;

    /**
     * Have we hit the lastEvent limit.
     */
    private boolean hitEventLimit;

    /**
     * Constructor.
     *
     * @param pData the dataSet
     */
    MoneyWiseArchiveCache(final MoneyWiseDataSet pData) {
        /* Store lists */
        theData = pData;
        theList = theData.getTransactions();

        /* Create the maps */
        theNameMap = new HashMap<>();
        theCategoryMap = new HashMap<>();
        theYears = new ArrayList<>();
    }

    /**
     * Enable filtering.
     */
    void enableFiltering() {
        enableFiltering = true;
    }

    /**
     * Set lastEvent.
     *
     * @param pLastEvent the last event date
     */
    void setLastEvent(final OceanusDate pLastEvent) {
        theLastEvent = pLastEvent;
    }

    /**
     * Check valid date.
     *
     * @param pDate the date
     * @return true/false
     */
    boolean checkDate(final OceanusDate pDate) {
        return theLastEvent == null || theLastEvent.compareTo(pDate) >= 0;
    }

    /**
     * Did we hit the event limit?
     *
     * @return true/false
     */
    boolean hitEventLimit() {
        return hitEventLimit;
    }

    /**
     * Add a year to the front of the list.
     *
     * @param pName the range name
     */
    void addYear(final String pName) {
        final MoneyWiseArchiveYear myYear = new MoneyWiseArchiveYear(pName);
        theYears.add(myYear);
    }

    /**
     * Get the iterator.
     *
     * @return the iterator
     */
    ListIterator<MoneyWiseArchiveYear> getIterator() {
        return theYears.listIterator();
    }

    /**
     * Obtain the reverse iterator of the years.
     *
     * @return the iterator.
     */
    ListIterator<MoneyWiseArchiveYear> reverseIterator() {
        return theYears.listIterator(getNumYears());
    }

    /**
     * Get the number of years.
     *
     * @return the number of years
     */
    int getNumYears() {
        return theYears.size();
    }

    /**
     * Build transaction.
     *
     * @param pAmount     the amount
     * @param pReconciled is the transaction reconciled?
     * @return the new transaction
     * @throws OceanusException on error
     */
    MoneyWiseTransaction buildTransaction(final String pAmount,
                                          final boolean pReconciled) throws OceanusException {
        /* Build data values */
        final PrometheusDataValues myValues = new PrometheusDataValues(MoneyWiseTransaction.OBJECT_NAME);
        myValues.addValue(MoneyWiseBasicResource.MONEYWISEDATA_FIELD_DATE, theDate);
        myValues.addValue(MoneyWiseBasicDataType.TRANSCATEGORY, theCategory);
        myValues.addValue(MoneyWiseBasicResource.TRANSACTION_DIRECTION, theDirection);
        myValues.addValue(MoneyWiseBasicResource.TRANSACTION_ACCOUNT, theAccount);
        myValues.addValue(MoneyWiseBasicResource.TRANSACTION_PARTNER, thePartner);
        myValues.addValue(MoneyWiseBasicResource.TRANSACTION_RECONCILED, pReconciled);
        if (pAmount != null) {
            myValues.addValue(MoneyWiseBasicResource.TRANSACTION_AMOUNT, pAmount);
        }

        if (filterTransaction(myValues)) {
            return null;
        }

        /* Add the value into the list */
        final MoneyWiseTransaction myTrans = theList.addValuesItem(myValues);

        /* If we were not a child */
        if (!isSplit) {
            /* Note the last parent */
            theLastParent = myTrans;
        }

        /* return the new transaction */
        return myTrans;
    }

    /**
     * Is the debit reversed?
     *
     * @return true/false
     */
    boolean isDebitReversed() {
        return isDebitReversed;
    }

    /**
     * Is the transaction recursive?
     *
     * @return true/false
     */
    boolean isRecursive() {
        return theLastDebit.equals(theLastCredit);
    }

    /**
     * should we filter this transaction?
     *
     * @param pTrans the transaction
     * @return true/false
     */
    boolean filterTransaction(final PrometheusDataValues pTrans) {
        return enableFiltering
                && (filterAsset(pTrans, MoneyWiseBasicResource.TRANSACTION_ACCOUNT)
                || filterAsset(pTrans, MoneyWiseBasicResource.TRANSACTION_PARTNER));
    }

    /**
     * Should we filter this asset?
     *
     * @param pTrans the transaction values
     * @param pAsset the asset
     * @return true/false
     */
    private boolean filterAsset(final PrometheusDataValues pTrans,
                                final MetisDataFieldId pAsset) {
        final MoneyWiseTransAsset myAsset = pTrans.getValue(pAsset, MoneyWiseTransAsset.class);
        switch (myAsset.getAssetType()) {
            case DEPOSIT:
            case CASH:
            case PAYEE:
            case LOAN:
                return false;
            default:
                return true;
        }
    }

    /**
     * Resolve Values.
     *
     * @param pDate     the date of the transaction
     * @param pDebit    the name of the debit object
     * @param pCredit   the name of the credit object
     * @param pCategory the name of the category object
     * @return continue true/false
     * @throws OceanusException on error
     */
    boolean resolveValues(final OceanusDate pDate,
                          final String pDebit,
                          final String pCredit,
                          final String pCategory) throws OceanusException {
        /* If the Date is null */
        if (pDate == null) {
            /* Resolve child values */
            resolveChildValues(pDebit, pCredit, pCategory);
            return true;
        }

        /* If the date is too late */
        if (!checkDate(pDate)) {
            /* reject the transaction */
            hitEventLimit = true;
            return false;
        }

        /* Note that there is no split */
        isSplit = Boolean.FALSE;
        theParent = null;

        /* Store the Date */
        theDate = pDate;

        /* Resolve the names */
        theLastDebit = theNameMap.get(pDebit);
        theLastCredit = theNameMap.get(pCredit);
        theCategory = theCategoryMap.get(pCategory);

        /* Check resolution */
        checkResolution(pDebit, pCredit, pCategory);

        /* If the category is portfolio transfer */
        if (theCategory.isCategoryClass(MoneyWiseTransCategoryClass.PORTFOLIOXFER)) {
            /* Adjust maps to reflect the transfer */
            resolvePortfolioXfer(theData, theLastDebit, theLastCredit);
        }

        /* Resolve assets */
        resolveAssets();
        return true;
    }

    /**
     * Resolve Child Values.
     *
     * @param pDebit    the name of the debit object
     * @param pCredit   the name of the credit object
     * @param pCategory the name of the category object
     * @throws OceanusException on error
     */
    private void resolveChildValues(final String pDebit,
                                    final String pCredit,
                                    final String pCategory) throws OceanusException {
        /* Handle no LastParent */
        if (theLastParent == null) {
            throw new MoneyWiseDataException(theDate, "Missing parent transaction");
        }

        /* Note that there is a split */
        isSplit = Boolean.TRUE;
        theParent = theLastParent;

        /* Resolve the debit and credit */
        final Object myDebit = pDebit == null
                ? theLastDebit
                : theNameMap.get(pDebit);
        final Object myCredit = pCredit == null
                ? theLastCredit
                : theNameMap.get(pCredit);

        /* Store last credit and debit */
        theLastDebit = myDebit;
        theLastCredit = myCredit;

        /* Resolve the category */
        theCategory = theCategoryMap.get(pCategory);

        /* Check resolution */
        checkResolution(pDebit, pCredit, pCategory);

        /* Resolve assets */
        resolveAssets();
    }

    /**
     * Resolve assets.
     *
     * @throws OceanusException on error
     */
    private void resolveAssets() throws OceanusException {
        final boolean isDebitHolding = theLastDebit instanceof MoneyWiseSecurityHolding;
        final boolean isCreditHolding = theLastCredit instanceof MoneyWiseSecurityHolding;

        /* Resolve debit and credit */
        final MoneyWiseTransAsset myDebit = (MoneyWiseTransAsset) theLastDebit;
        final MoneyWiseTransAsset myCredit = (MoneyWiseTransAsset) theLastCredit;

        /* Access asset types */
        final MoneyWiseAssetType myDebitType = myDebit.getAssetType();
        final MoneyWiseAssetType myCreditType = myCredit.getAssetType();

        /* Handle non-Asset debit */
        if (!myDebitType.isBaseAccount()) {
            /* Use credit as account */
            isDebitReversed = true;

            /* Handle non-Asset credit */
        } else if (!myCreditType.isBaseAccount()) {
            /* Use debit as account */
            isDebitReversed = false;

            /* Handle non-child transfer */
        } else if (!isSplit) {
            /* Flip values for StockRightsTaken and LoanInterest */
            switch (Objects.requireNonNull(theCategory.getCategoryTypeClass())) {
                case STOCKRIGHTSISSUE:
                    /* Use securityHolding as account */
                    isDebitReversed = !myDebitType.isSecurityHolding();
                    break;
                case LOANINTERESTEARNED:
                    /* Use credit as account */
                    isDebitReversed = !theData.newValidityChecks();
                    break;
                case LOANINTERESTCHARGED:
                case WRITEOFF:
                    /* Use credit as account */
                    isDebitReversed = theData.newValidityChecks();
                    break;
                default:
                    /* Use debit as account */
                    isDebitReversed = false;
                    break;
            }
        } else {
            /* Access parent assets */
            final MoneyWiseTransAsset myParAccount = theParent.getAccount();
            final MoneyWiseTransAsset myParPartner = theParent.getPartner();

            /* If we match the parent on debit */
            if (myDebit.equals(myParAccount)) {
                /* Use debit as account */
                isDebitReversed = false;

                /* else if we match credit account */
            } else if (myCredit.equals(myParAccount)) {
                /* Use credit as account */
                isDebitReversed = true;

                /* else don't match the parent account, so parent must be wrong */
            } else {
                /* Flip parent assets */
                theParent.flipAssets();

                /* Determine if debit is reversed */
                isDebitReversed = !myDebit.equals(myParPartner);
            }
        }

        /* Set up values */
        if (!isDebitReversed) {
            /* Use debit as account */
            theAccount = myDebit;
            thePartner = myCredit;
            theDirection = MoneyWiseAssetDirection.TO;
        } else {
            /* Use credit as account */
            theAccount = myCredit;
            thePartner = myDebit;
            theDirection = MoneyWiseAssetDirection.FROM;
        }

        /* Resolve portfolio */
        thePortfolio = null;
        if (isDebitHolding) {
            thePortfolio = ((MoneyWiseSecurityHolding) theLastDebit).getPortfolio();
            if (isCreditHolding) {
                final MoneyWisePortfolio myPortfolio = ((MoneyWiseSecurityHolding) theLastCredit).getPortfolio();
                if (!thePortfolio.equals(myPortfolio)) {
                    throw new MoneyWiseDataException(theDate, "Inconsistent portfolios");
                }
            }
        } else if (isCreditHolding) {
            thePortfolio = ((MoneyWiseSecurityHolding) theLastCredit).getPortfolio();
        }
    }

    /**
     * Check resolution.
     *
     * @param pDebit    the name of the debit object
     * @param pCredit   the name of the credit object
     * @param pCategory the name of the category object
     * @throws OceanusException on error
     */
    private void checkResolution(final String pDebit,
                                 final String pCredit,
                                 final String pCategory) throws OceanusException {
        /* Check debit resolution */
        if (theLastDebit == null) {
            throw new MoneyWiseDataException(pDebit, "Failed to resolve debit account on " + theDate);
        }

        /* Check credit resolution */
        if (theLastCredit == null) {
            throw new MoneyWiseDataException(pCredit, "Failed to resolve credit account on " + theDate);
        }

        /* Check category resolution */
        if (theCategory == null) {
            throw new MoneyWiseDataException(pCategory, "Failed to resolve category on " + theDate);
        }
    }

    /**
     * Declare asset.
     *
     * @param pAsset the asset to declare.
     * @throws OceanusException on error
     */
    void declareAsset(final MoneyWiseAssetBase pAsset) throws OceanusException {
        /* Access the asset name */
        final String myName = pAsset.getName();

        /* Check for name already exists */
        if (theNameMap.get(myName) != null) {
            throw new MoneyWiseDataException(pAsset, PrometheusDataItem.ERROR_DUPLICATE);
        }

        /* Store the asset */
        theNameMap.put(myName, pAsset);
    }

    /**
     * Declare category.
     *
     * @param pCategory the category to declare.
     * @throws OceanusException on error
     */
    void declareCategory(final MoneyWiseTransCategory pCategory) throws OceanusException {
        /* Access the asset name */
        final String myName = pCategory.getName();

        /* Check for name already exists */
        if (theCategoryMap.get(myName) != null) {
            throw new MoneyWiseDataException(pCategory, PrometheusDataItem.ERROR_DUPLICATE);
        }

        /* Store the category */
        theCategoryMap.put(myName, pCategory);
    }

    /**
     * Declare security holding.
     *
     * @param pSecurity  the security.
     * @param pPortfolio the portfolio
     * @throws OceanusException on error
     */
    void declareSecurityHolding(final MoneyWiseSecurity pSecurity,
                                final String pPortfolio) throws OceanusException {
        /* Access the name */
        final String myName = pSecurity.getName();

        /* Check for name already exists */
        if (theNameMap.get(myName) != null) {
            throw new MoneyWiseDataException(pSecurity, PrometheusDataItem.ERROR_DUPLICATE);
        }

        /* Store the asset */
        theNameMap.put(myName, new MoneyWiseSecurityHoldingDef(pSecurity, pPortfolio));
    }

    /**
     * Declare alias holding.
     *
     * @param pName      the security holding name
     * @param pAlias     the alias name.
     * @param pPortfolio the portfolio
     * @throws OceanusException on error
     */
    void declareAliasHolding(final String pName,
                             final String pAlias,
                             final String pPortfolio) throws OceanusException {
        /* Check for name already exists */
        final Object myHolding = theNameMap.get(pAlias);
        if (!(myHolding instanceof MoneyWiseSecurityHoldingDef myAliased)) {
            throw new MoneyWiseDataException(pAlias, "Aliased security not found");
        }

        /* Store the asset */
        theNameMap.put(pName, new MoneyWiseSecurityHoldingDef(myAliased.getSecurity(), pPortfolio));
    }

    /**
     * Resolve security holdings.
     *
     * @param pData the dataSet
     */
    void resolveSecurityHoldings(final MoneyWiseDataSet pData) {
        /* Access securityHoldingsMap and Portfolio list */
        final MoneyWiseSecurityHoldingMap myMap = pData.getPortfolios().getSecurityHoldingsMap();
        final MoneyWisePortfolioList myPortfolios = pData.getPortfolios();

        /* Loop through the name map */
        for (Entry<String, Object> myEntry : theNameMap.entrySet()) {
            /* If this is a security holding definition */
            final Object myValue = myEntry.getValue();
            if (myValue instanceof MoneyWiseSecurityHoldingDef myDef) {
                /* Access security holding */
                final MoneyWisePortfolio myPortfolio = myPortfolios.findItemByName(myDef.getPortfolio());
                final MoneyWiseSecurityHolding myHolding = myMap.declareHolding(myPortfolio, myDef.getSecurity());

                /* Replace definition in map */
                myEntry.setValue(myHolding);
            }
        }
    }


    /**
     * Process portfolio transfer.
     *
     * @param pData   the dataSet
     * @param pSource the source asset
     * @param pTarget the target asset
     * @throws OceanusException on error
     */
    private void resolvePortfolioXfer(final MoneyWiseDataSet pData,
                                      final Object pSource,
                                      final Object pTarget) throws OceanusException {
        /* Target must be portfolio */
        if (!(pTarget instanceof MoneyWisePortfolio myPortfolio)) {
            throw new MoneyWiseDataException(pTarget, "Inconsistent portfolios");
        }

        final MoneyWiseSecurityHoldingMap myMap = pData.getPortfolios().getSecurityHoldingsMap();

        /* Loop through the name map */
        for (Entry<String, Object> myEntry : theNameMap.entrySet()) {
            /* If this is a security holding definition */
            final Object myValue = myEntry.getValue();
            if (myValue instanceof MoneyWiseSecurityHolding myHolding) {
                /* If this holding needs updating */
                if (pSource.equals(myHolding) || pSource.equals(myHolding.getPortfolio())) {
                    /* Change the holding */
                    myHolding = myMap.declareHolding(myPortfolio, myHolding.getSecurity());

                    /* Replace definition in map */
                    myEntry.setValue(myHolding);
                }
            }
        }
    }

    /**
     * Security Holding Definition.
     */
    private static final class MoneyWiseSecurityHoldingDef {
        /**
         * Security.
         */
        private final MoneyWiseSecurity theSecurity;

        /**
         * Portfolio.
         */
        private final String thePortfolio;

        /**
         * Constructor.
         *
         * @param pSecurity  the security
         * @param pPortfolio the portfolio
         */
        private MoneyWiseSecurityHoldingDef(final MoneyWiseSecurity pSecurity,
                                            final String pPortfolio) {
            /* Store parameters */
            theSecurity = pSecurity;
            thePortfolio = pPortfolio;
        }

        /**
         * Obtain security.
         *
         * @return the security
         */
        public MoneyWiseSecurity getSecurity() {
            return theSecurity;
        }

        /**
         * Obtain portfolio.
         *
         * @return the portfolio
         */
        public String getPortfolio() {
            return thePortfolio;
        }
    }
}