package com.fanitas.evelyn.core.impl.ref;

import com.fanitas.evelyn.core.State;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionAccounting;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.services.cis.ExternalCurrencyReference;
import com.r35157.assetaz.valuetypes.CurrencyType;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import com.r35157.libs.raydium.*;
import com.r35157.libs.valuetypes.basic.*;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static com.r35157.assetaz.services.cis.ExternalCurrencyReference.SOLANA_MINT_NAMESPACE;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SYRUPUSDC_ID;

public class StateImpl implements State {

    public StateImpl(
            Raydium raydium,
            CurrencyIdentityService currencyIdentityService
    ) throws IOException, InterruptedException {
        this.raydium = raydium;
        this.currencyIdentityService = Objects.requireNonNull(
                currencyIdentityService,
                "currencyIdentityService"
        );
        update();
    }

    @Override
    public ΩmilliSecondsΩ getIterationInterval() {
        return iterationInterval;
    }

    @Override
    public ΩSyrupAmountΩ getSyrupOwnedByEvelyn() {
        return syrupOwnedByEvelyn;
    }

    @Override
    public ΩSolanaAddressΩ getSolanaAddressForEvelynIOU() {
        return solanaAddressForEvelynIOU;
    }

    @Override
    public ΩSolanaAddressΩ getSolanaAddressForEvelynIOUBurner() {
        return solanaAddressForEvelynIOUBurner;
    }

    @Override
    public ΩRaydiumLiquidityPoolIdΩ getRaydiumPoolId() {
        return raydiumPoolId;
    }

    @Override
    public BigDecimal getCurveWidth() {
        return curveWidth;
    }

    @Override
    public Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> getLiquidityPositions() {
        return liquidityPositions;
    }

    public void update() throws IOException, InterruptedException {
        System.out.print("Updating from config file...");
        updateStateFromPersistence();
        System.out.println(" Done.");
        System.out.print("Updating from Raydium...");
        updateStateFromRaydium();
        System.out.println(" Done.");
        System.out.print("Updating from positions file...");
        updateStateFromPositionsFile();
        System.out.println(" Done.");
    }

    private void updateStateFromPersistence() throws IOException {
        List<String> lines = Files.readAllLines(
                PATH_EVELYN_STATE,
                StandardCharsets.UTF_8
        );
        int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
                PATH_EVELYN_STATE,
                lines,
                SUPPORTED_EVELYN_FORMAT_VERSION
        );
        Iterator<String> lineIterator = lines.subList(
                formatVersionLineIndex + 1,
                lines.size()
        ).iterator();

        iterationInterval = readIterationInterval(lineIterator);
        syrupOwnedByEvelyn = readSyrupOwnedByEvelyn(lineIterator);
        solanaAddressForEvelynIOU = readSolanaAddress(lineIterator);
        solanaAddressForEvelynIOUBurner = readSolanaAddress(lineIterator);
        raydiumPoolId = readRaydiumPoolId(lineIterator);
        curveWidth = readCurveWidth(lineIterator);
    }

    private void updateStateFromRaydium() throws IOException, InterruptedException {
        Set<ΩRaydiumLiquidityPoolPositionNftIdΩ> positionNftIds =
                raydium.fetchConcentratedPositionNftIds(solanaAddressForEvelynIOU);

        Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> positions =
                new HashMap<>();

        System.out.println("Detected " + positionNftIds.size() + " position(s)...");
        for (ΩRaydiumLiquidityPoolPositionNftIdΩ positionNftId : positionNftIds) {
            RaydiumConcentratedPositionState positionState =
                    raydium.fetchConcentratedPositionState(positionNftId);

            RaydiumConcentratedPoolInfo poolInfo =
                    raydium.fetchConcentratedPoolInfo(positionState.poolId());

            RaydiumConcentratedPoolState poolState =
                    raydium.fetchConcentratedPoolState(positionState.poolId());

            CurrencyType currencyTypeA = resolveSolanaMint(poolInfo.mintA());
            CurrencyType currencyTypeB = resolveSolanaMint(poolInfo.mintB());

            int mintBDecimals = poolInfo.mintBDecimals();
            Range<AssetPrice> priceRange = raydium.calculateConcentratedPositionPriceRange(
                    positionState,
                    poolInfo,
                    mintBDecimals
            );

            RaydiumLiquidityPoolTokenAmounts tokenAmounts =
                    raydium.calculateConcentratedPositionTokenAmounts(
                            positionState,
                            poolInfo,
                            poolState
                    );

            MoneyAmount amountA = new MoneyAmount(tokenAmounts.amountA(), currencyTypeA);
            MoneyAmount amountB = new MoneyAmount(tokenAmounts.amountB(), currencyTypeB);
            RaydiumLiquidityPoolPositionConcentrated position =
                    new RaydiumLiquidityPoolPositionConcentrated(
                            positionState.poolId(),
                            positionNftId,
                            priceRange,
                            amountA,
                            amountB,
                            null // The accounting info will be added from 'conf/positions.conf' later.
                    );

            positions.put(positionNftId, position);
            System.out.println("   Added '" + position.nftId() + "': "
                    + "Range:" + position.priceRange()
                    + ", Liquidity:" + amountA + "," + amountB
            );
        }

        liquidityPositions = Map.copyOf(positions);
    }

    private CurrencyType resolveSolanaMint(ΩSPLMintAddressΩ mintAddress) {
        return currencyIdentityService.resolve(new ExternalCurrencyReference(
                SOLANA_MINT_NAMESPACE,
                mintAddress,
                null
        ));
    }

    private void updateStateFromPositionsFile() throws IOException {
        Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> accountingEntries =
                readPositionAccounting();

        Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> updatedEntries =
                new HashMap<>(liquidityPositions);

        for (RaydiumLiquidityPoolPositionAccounting accountingEntry : accountingEntries.values()) {
            String nftId = accountingEntry.nftId();
            RaydiumLiquidityPoolPositionConcentrated position = updatedEntries.get(nftId);

            if (position == null) {
                System.out.println(
                        "WARNING: File 'conf/positions.conf' contains accounting for an NFT '" + nftId
                                + "' that was not discovered in the wallet!");
                continue;
            }

            RaydiumLiquidityPoolPositionConcentrated updatedPosition =
                    new RaydiumLiquidityPoolPositionConcentrated(
                            position.poolId(),
                            position.nftId(),
                            position.priceRange(),
                            position.amountMintA(),
                            position.amountMintB(),
                            accountingEntry
                    );

            updatedEntries.put(nftId, updatedPosition);
        }

        liquidityPositions = Map.copyOf(updatedEntries);
    }

    private Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> readPositionAccounting()
            throws IOException {
        Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> result = new HashMap<>();
        List<String> lines = Files.readAllLines(PATH_POSITIONS, StandardCharsets.UTF_8);
        int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
                PATH_POSITIONS,
                lines,
                SUPPORTED_POSITIONS_FORMAT_VERSION
        );

        for (int lineIndex = formatVersionLineIndex + 1;
             lineIndex < lines.size();
             lineIndex++) {
            String line = lines.get(lineIndex).trim();

            if (line.isEmpty() || line.startsWith("#")) {
                continue;
            }

            // Remove comments at end of line
            int index = line.indexOf('#');
            if(index > -1) {
                line = line.substring(0, index).trim();
            }

            RaydiumLiquidityPoolPositionAccounting accounting = mapPositionAccounting(line);

            RaydiumLiquidityPoolPositionAccounting previous = result.put(
                    accounting.nftId(),
                    accounting
            );

            if (previous != null) {
                throw new IOException("Duplicate position accounting for NFT id: " + accounting.nftId());
            }
        }

        return Map.copyOf(result);
    }

    private RaydiumLiquidityPoolPositionAccounting mapPositionAccounting(String line) throws IOException {
        String[] values = line.split(";");

        if (values.length != 5) {
            String errormsg = "Expected position accounting line with 5 fields: "
                    + "'NFTId ; AddedMintA ; AddedMintB ; BorrowedMintA ; BorrowedMintB'. "
                    + "Line was: '" + line + "'";
            throw new IOException(errormsg);
        }

        ΩRaydiumLiquidityPoolPositionNftIdΩ nftId = values[0].trim();
        ΩAmountΩ addedMintA = new BigDecimal(values[1].trim());
        ΩAmountΩ addedMintB = new BigDecimal(values[2].trim());
        ΩAmountΩ borrowedMintA = new BigDecimal(values[3].trim());
        ΩAmountΩ borrowedMintB = new BigDecimal(values[4].trim());

        return new RaydiumLiquidityPoolPositionAccounting(
                nftId,
                addedMintA,
                addedMintB,
                borrowedMintA,
                borrowedMintB
        );
    }

    private ΩSolanaAddressΩ readSolanaAddress(Iterator<String> lineIterator) throws IOException {
        ΩSolanaAddressΩ sa = readLineFromFile(lineIterator);
        return sa;
    }

    private ΩRaydiumLiquidityPoolIdΩ readRaydiumPoolId(Iterator<String> lineIterator) throws IOException {
        ΩRaydiumLiquidityPoolIdΩ rlpid = readLineFromFile(lineIterator);
        return rlpid;
    }

    private ΩCurveWidthΩ readCurveWidth(Iterator<String> lineIterator) throws IOException {
        String cwStr = readLineFromFile(lineIterator);
        ΩCurveWidthΩ cw = new ΩCurveWidthΩ(cwStr);
        return cw;
    }

    private ΩmilliSecondsΩ readIterationInterval(Iterator<String> lineIterator) throws IOException {
        String iterationIntervalStr = readLineFromFile(lineIterator);
        ΩmilliSecondsΩ millis = Long.parseLong(iterationIntervalStr) * 1000;
        return millis;
    }

    private ΩSyrupAmountΩ readSyrupOwnedByEvelyn(Iterator<String> lineIterator) throws IOException {
        String syrupOwnedByEvelynStr = readLineFromFile(lineIterator);
        CurrencyType ct = currencyIdentityService.resolve(SYRUPUSDC_ID);
        ΩSyrupAmountΩ ma = stringToMoneyAmount(syrupOwnedByEvelynStr, ct);
        return ma;
    }

    private static String readLineFromFile(Iterator<String> lineIterator) {
        while (lineIterator.hasNext()) {
            String line = lineIterator.next();
            line = line.trim();

            if (line.isEmpty() || line.startsWith("#")) {
                continue;
            }
            return line;
        }
        return null;
    }

    private MoneyAmount bigDecimalToMoneyAmount(BigDecimal bd, CurrencyType ct) {
        MoneyAmount ma = new MoneyAmount(bd, ct);
        return ma;
    }

    private MoneyAmount stringToMoneyAmount(String str, CurrencyType ct) {
        ΩAmountΩ a = new ΩAmountΩ(str);
        MoneyAmount ma = new MoneyAmount(a, ct);
        return ma;
    }

    private static final Path PATH_EVELYN_STATE = Path.of("conf/evelyn.conf");
    private static final Path PATH_POSITIONS = Path.of("conf/positions.conf");
    private static final int SUPPORTED_EVELYN_FORMAT_VERSION = 1;
    private static final int SUPPORTED_POSITIONS_FORMAT_VERSION = 1;
    private final Raydium raydium;
    private final CurrencyIdentityService currencyIdentityService;

    private ΩmilliSecondsΩ iterationInterval;
    private ΩSyrupAmountΩ syrupOwnedByEvelyn;
    private ΩSolanaAddressΩ solanaAddressForEvelynIOU;
    private ΩSolanaAddressΩ solanaAddressForEvelynIOUBurner;
    private ΩRaydiumLiquidityPoolIdΩ raydiumPoolId;
    private ΩCurveWidthΩ curveWidth;
    private Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityPositions;
}
