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

import com.fanitas.evelyn.core.*;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;

import com.r35157.libs.basic.Pair;
import com.r35157.libs.basic.Triblet;
import com.r35157.libs.raydium.Raydium;
import com.r35157.libs.raydium.RaydiumLiquidityPoolPrice;
import com.r35157.libs.solana.SPLTokenHolding;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaConstants;
import com.r35157.libs.valuetypes.basic.AssetPrice;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import com.r35157.libs.valuetypes.basic.TradingPair;
import com.r35157.assetaz.services.ticker.PriceObservation;
import com.r35157.assetaz.services.ticker.TickerService;
import com.r35157.nenjim.component.NenjimApplication;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.text.Normalizer;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;

import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM;

public class EvelynImpl implements Evelyn, NenjimApplication {

    public EvelynImpl(
            @NotNull String instanceName,
            @NotNull TickerService tickerService,
            @NotNull TradingPair eveUsdtTradingPair,
            @NotNull Clock clock
    ) {
        this(
                instanceName,
                tickerService,
                eveUsdtTradingPair,
                clock,
                SAMPLE_DELAY_MINUTES,
                TimeUnit.MINUTES,
                DATA_ROOT
        );
    }

    EvelynImpl(
            @NotNull String instanceName,
            @NotNull TickerService tickerService,
            @NotNull TradingPair eveUsdtTradingPair,
            @NotNull Clock clock,
            long sampleDelay,
            @NotNull TimeUnit sampleDelayUnit,
            @NotNull Path dataRoot
    ) {
        TickerService validatedTickerService = Objects.requireNonNull(
                tickerService,
                "tickerService"
        );
        TradingPair validatedTradingPair = Objects.requireNonNull(
                eveUsdtTradingPair,
                "eveUsdtTradingPair"
        );
        Clock validatedClock = Objects.requireNonNull(clock, "clock");
        if (sampleDelay <= 0) {
            throw new IllegalArgumentException("sampleDelay must be positive");
        }
        TimeUnit validatedDelayUnit = Objects.requireNonNull(
                sampleDelayUnit,
                "sampleDelayUnit"
        );
        Path validatedDataRoot = Objects.requireNonNull(dataRoot, "dataRoot").normalize();
        String normalizedInstanceName = validateInstanceName(instanceName);
        Path validatedInstancePath = validatedDataRoot
                .resolve(normalizedInstanceName)
                .normalize();
        if (!validatedDataRoot.equals(validatedInstancePath.getParent())) {
            throw new IllegalArgumentException(
                    "Evelyn instance name must resolve to a direct child of "
                            + validatedDataRoot + ": " + normalizedInstanceName
            );
        }
        Path validatedStatusHistoryPath = validatedInstancePath.resolve(
                STATUS_HISTORY_FILENAME
        );
        String reservationKey = normalizedInstanceName.toLowerCase(Locale.ROOT);

        if (!RESERVED_INSTANCE_NAMES.add(reservationKey)) {
            throw new IllegalArgumentException(
                    "Evelyn instance name is already reserved in this JVM: "
                            + normalizedInstanceName
            );
        }

        this.instanceName = normalizedInstanceName;
        this.reservationKey = reservationKey;
        this.instancePath = validatedInstancePath;
        this.statusHistoryPath = validatedStatusHistoryPath;
        this.tickerService = validatedTickerService;
        this.eveUsdtTradingPair = validatedTradingPair;
        this.clock = validatedClock;
        this.sampleDelay = sampleDelay;
        this.sampleDelayUnit = validatedDelayUnit;

        statusIndexHistory = new ArrayList<>();
    }

    @Override
    public void executeService() throws Exception {
        //handleRebalancingProposal();
    }

    @Override
    public synchronized void start() {
        if (closeRequested || lifecycleState == LifecycleState.CLOSED) {
            throw new IllegalStateException(
                    "Closing or closed Evelyn instance cannot be started: "
                            + instanceName
            );
        }
        if (lifecycleState != LifecycleState.STOPPED) {
            throw new IllegalStateException(
                    "Evelyn instance cannot start while in state "
                            + lifecycleState + ": " + instanceName
            );
        }

        lifecycleState = LifecycleState.STARTING;
        LoadedHistory loadedHistory;
        try {
            loadedHistory = loadStatusHistory();
        } catch (IOException | RuntimeException exception) {
            clearStatusIndexHistory();
            lifecycleState = LifecycleState.STOPPED;
            throw new IllegalStateException(
                    "Could not start Evelyn instance '" + instanceName
                            + "' from " + statusHistoryPath + ": "
                            + exception.getMessage(),
                    exception
            );
        }

        ScheduledExecutorService newScheduler;
        try {
            newScheduler = Executors.newSingleThreadScheduledExecutor(
                    runnable -> {
                        Thread thread = new Thread(
                                runnable,
                                "evelyn-price-index-sampler-" + instanceName
                        );
                        thread.setDaemon(true);
                        return thread;
                    }
            );
        } catch (RuntimeException exception) {
            clearStatusIndexHistory();
            lifecycleState = LifecycleState.STOPPED;
            throw new IllegalStateException(
                    "Could not create Evelyn sampling executor for instance '"
                            + instanceName + "'",
                    exception
            );
        }
        try {
            synchronized (statusIndexHistory) {
                statusIndexHistory.clear();
                statusIndexHistory.addAll(loadedHistory.points());
                lastPersistedTimestamp = loadedHistory.lastTimestamp();
            }
            statusIndexScheduler = newScheduler;
            lifecycleState = LifecycleState.STARTED;
            newScheduler.scheduleWithFixedDelay(
                    this::sampleStatusIndexSafely,
                    0,
                    sampleDelay,
                    sampleDelayUnit
            );
        } catch (RuntimeException exception) {
            lifecycleState = LifecycleState.STOPPING;
            newScheduler.shutdownNow();
            statusIndexScheduler = null;
            clearStatusIndexHistory();
            lifecycleState = LifecycleState.STOPPED;
            throw new IllegalStateException(
                    "Could not start Evelyn sampling for instance '"
                            + instanceName + "'",
                    exception
            );
        }
    }

    @Override
    public void stop() {
        ScheduledExecutorService schedulerToStop;
        synchronized (this) {
            if (lifecycleState == LifecycleState.CLOSED
                    || lifecycleState == LifecycleState.STOPPED) {
                clearStatusIndexHistory();
                return;
            }
            if (lifecycleState != LifecycleState.STARTED
                    && lifecycleState != LifecycleState.TERMINATION_FAILED) {
                throw new IllegalStateException(
                        "Evelyn instance cannot stop while in state "
                                + lifecycleState + ": " + instanceName
                );
            }
            lifecycleState = LifecycleState.STOPPING;
            schedulerToStop = statusIndexScheduler;
        }

        schedulerToStop.shutdownNow();
        boolean terminated;
        try {
            terminated = schedulerToStop.awaitTermination(
                    TERMINATION_TIMEOUT_SECONDS,
                    TimeUnit.SECONDS
            );
        } catch (InterruptedException exception) {
            schedulerToStop.shutdownNow();
            Thread.currentThread().interrupt();
            synchronized (this) {
                lifecycleState = LifecycleState.TERMINATION_FAILED;
            }
            throw new IllegalStateException(
                    "Interrupted while stopping Evelyn instance '"
                            + instanceName + "'; name reservation retained",
                    exception
            );
        }

        if (!terminated) {
            synchronized (this) {
                lifecycleState = LifecycleState.TERMINATION_FAILED;
            }
            throw new IllegalStateException(
                    "Evelyn sampling thread did not terminate for instance '"
                            + instanceName + "'; name reservation retained"
            );
        }

        clearStatusIndexHistory();
        synchronized (this) {
            statusIndexScheduler = null;
            lifecycleState = LifecycleState.STOPPED;
        }
    }

    @Override
    public void close() {
        synchronized (this) {
            if (lifecycleState == LifecycleState.CLOSED) {
                return;
            }
            closeRequested = true;
        }

        stop();

        synchronized (this) {
            if (lifecycleState != LifecycleState.STOPPED
                    || statusIndexScheduler != null) {
                throw new IllegalStateException(
                        "Cannot close Evelyn instance while sampling termination "
                                + "is unconfirmed: " + instanceName
                );
            }
            lifecycleState = LifecycleState.CLOSED;
            RESERVED_INSTANCE_NAMES.remove(reservationKey);
        }
    }

    @Override
    public @NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory() {
        synchronized (statusIndexHistory) {
            return List.copyOf(statusIndexHistory);
        }
    }

    private void clearStatusIndexHistory() {
        synchronized (statusIndexHistory) {
            statusIndexHistory.clear();
            lastPersistedTimestamp = null;
        }
    }

    private LoadedHistory loadStatusHistory() throws IOException {
        validateStorage();
        List<String> lines = Files.readAllLines(
                statusHistoryPath,
                StandardCharsets.UTF_8
        );
        int formatVersionLineIndex = validateStatusFormatVersion(lines);
        List<EvelynStatusIndexPoint> loadedPoints = new ArrayList<>();
        Instant previousTimestamp = null;

        for (int lineIndex = formatVersionLineIndex + 1;
                lineIndex < lines.size();
                lineIndex++) {
            String rawLine = lines.get(lineIndex);
            String data = removeComment(rawLine).trim();
            if (data.isEmpty()) {
                continue;
            }

            EvelynStatusIndexPoint point = parseStatusRecord(
                    data,
                    rawLine,
                    lineIndex
            );
            if (previousTimestamp != null
                    && point.timestamp().isBefore(previousTimestamp)) {
                throw malformedStatusHistory(
                        lineIndex,
                        rawLine,
                        "timestamp is earlier than the preceding record",
                        null
                );
            }
            loadedPoints.add(point);
            previousTimestamp = point.timestamp();
        }

        return new LoadedHistory(List.copyOf(loadedPoints), previousTimestamp);
    }

    private void validateStorage() throws IOException {
        requireStorageCondition(
                Files.exists(instancePath),
                instancePath,
                "instance directory does not exist"
        );
        requireStorageCondition(
                Files.isDirectory(instancePath),
                instancePath,
                "instance path is not a directory"
        );
        requireStorageCondition(
                Files.isReadable(instancePath),
                instancePath,
                "instance directory is not readable"
        );
        requireStorageCondition(
                Files.isWritable(instancePath),
                instancePath,
                "instance directory is not writable"
        );
        requireStorageCondition(
                Files.exists(statusHistoryPath),
                statusHistoryPath,
                "status history file does not exist"
        );
        requireStorageCondition(
                Files.isRegularFile(statusHistoryPath),
                statusHistoryPath,
                "status history path is not a regular file"
        );
        requireStorageCondition(
                Files.isReadable(statusHistoryPath),
                statusHistoryPath,
                "status history file is not readable"
        );
        requireStorageCondition(
                Files.isWritable(statusHistoryPath),
                statusHistoryPath,
                "status history file is not writable"
        );
    }

    private static void requireStorageCondition(
            boolean condition,
            Path path,
            String reason
    ) throws IOException {
        if (!condition) {
            throw new IOException(path + ": " + reason);
        }
    }

    private int validateStatusFormatVersion(List<String> lines) throws IOException {
        int firstActualEntryIndex = -1;
        int declarationIndex = -1;
        int declarationCount = 0;
        int declaredVersion = -1;

        for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
            String rawLine = lines.get(lineIndex);
            String trimmedLine = rawLine.trim();
            if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
                continue;
            }
            if (firstActualEntryIndex < 0) {
                firstActualEntryIndex = lineIndex;
            }
            if (!trimmedLine.startsWith(FORMAT_VERSION_KEY)) {
                continue;
            }

            declarationCount++;
            if (declarationCount > 1) {
                throw malformedStatusHistory(
                        lineIndex,
                        rawLine,
                        "duplicate " + FORMAT_VERSION_KEY + " declaration",
                        null
                );
            }
            declarationIndex = lineIndex;
            String expectedPrefix = FORMAT_VERSION_KEY + "=";
            if (!trimmedLine.startsWith(expectedPrefix)
                    || trimmedLine.length() == expectedPrefix.length()) {
                throw malformedStatusHistory(
                        lineIndex,
                        rawLine,
                        "malformed " + FORMAT_VERSION_KEY
                                + " declaration; expected "
                                + FORMAT_VERSION_KEY + "=<positive integer>",
                        null
                );
            }
            try {
                declaredVersion = Integer.parseInt(
                        trimmedLine.substring(expectedPrefix.length())
                );
            } catch (NumberFormatException exception) {
                throw malformedStatusHistory(
                        lineIndex,
                        rawLine,
                        "malformed " + FORMAT_VERSION_KEY
                                + " declaration; expected "
                                + FORMAT_VERSION_KEY + "=<positive integer>",
                        exception
                );
            }
            if (declaredVersion <= 0) {
                throw malformedStatusHistory(
                        lineIndex,
                        rawLine,
                        FORMAT_VERSION_KEY + " must be a positive integer",
                        null
                );
            }
        }

        if (declarationCount == 0) {
            if (firstActualEntryIndex >= 0) {
                throw malformedStatusHistory(
                        firstActualEntryIndex,
                        lines.get(firstActualEntryIndex),
                        "missing " + FORMAT_VERSION_KEY
                                + " declaration; it must be the first actual entry",
                        null
                );
            }
            throw new IOException(
                    statusHistoryPath + ": missing " + FORMAT_VERSION_KEY
                            + " declaration; it must be the first actual entry"
            );
        }
        if (declarationIndex != firstActualEntryIndex) {
            throw malformedStatusHistory(
                    declarationIndex,
                    lines.get(declarationIndex),
                    FORMAT_VERSION_KEY + " must be the first actual entry",
                    null
            );
        }
        if (declaredVersion != SUPPORTED_STATUS_HISTORY_FORMAT_VERSION) {
            throw malformedStatusHistory(
                    declarationIndex,
                    lines.get(declarationIndex),
                    "unsupported status history format version "
                            + declaredVersion + "; supported version is "
                            + SUPPORTED_STATUS_HISTORY_FORMAT_VERSION,
                    null
            );
        }
        return declarationIndex;
    }

    private EvelynStatusIndexPoint parseStatusRecord(
            String data,
            String rawLine,
            int lineIndex
    ) throws IOException {
        String[] fields = data.split(":", -1);
        if (fields.length != STATUS_RECORD_FIELD_COUNT) {
            throw malformedStatusHistory(
                    lineIndex,
                    rawLine,
                    "expected exactly " + STATUS_RECORD_FIELD_COUNT
                            + " colon-separated fields",
                    null
            );
        }

        try {
            LocalDateTime localDateTime = LocalDateTime.parse(
                    fields[0],
                    STATUS_TIMESTAMP_FORMATTER
            );
            Instant timestamp = localDateTime.toInstant(ZoneOffset.UTC);
            int recordType = Integer.parseInt(fields[1]);
            if (recordType != EVELYN_IOU_TOKEN_PRICE_INDEX_RECORD_TYPE) {
                throw new UnknownStatusRecordTypeException(recordType);
            }
            BigDecimal evelynPriceIndex = new BigDecimal(fields[2]);
            return new EvelynStatusIndexPoint(
                    timestamp,
                    evelynPriceIndex,
                    BigDecimal.ZERO,
                    BigDecimal.ZERO,
                    BigDecimal.ZERO,
                    BigDecimal.ZERO,
                    BigDecimal.ZERO
            );
        } catch (UnknownStatusRecordTypeException exception) {
            throw malformedStatusHistory(
                    lineIndex,
                    rawLine,
                    "unknown status record type " + exception.recordType,
                    exception
            );
        } catch (RuntimeException exception) {
            throw malformedStatusHistory(
                    lineIndex,
                    rawLine,
                    "invalid status record",
                    exception
            );
        }
    }

    private IOException malformedStatusHistory(
            int lineIndex,
            String rawLine,
            String reason,
            Exception cause
    ) {
        return new IOException(
                "Malformed Evelyn status history in " + statusHistoryPath
                        + " at line " + (lineIndex + 1) + ": " + rawLine
                        + " (" + reason + ")",
                cause
        );
    }

    private void sampleStatusIndexSafely() {
        Instant samplingInstant = null;
        try {
            samplingInstant = clock.instant().truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
            PriceObservation observation = tickerService.getLatestPrice(eveUsdtTradingPair);
            BigDecimal actualPrice = observation.price().price();
            BigDecimal evelynPriceIndex = EvelynPriceIndexCalculator.calculateIndex(
                    actualPrice,
                    samplingInstant
            );
            EvelynStatusIndexPoint point = new EvelynStatusIndexPoint(
                    samplingInstant,
                    evelynPriceIndex,
                    BigDecimal.ZERO,
                    BigDecimal.ZERO,
                    BigDecimal.ZERO,
                    BigDecimal.ZERO,
                    BigDecimal.ZERO
            );
            persistAndPublish(point);
        } catch (IOException exception) {
            log.error(
                    "Could not persist Evelyn Price Index: instance={}, path={}, samplingInstant={}",
                    instanceName,
                    statusHistoryPath,
                    samplingInstant,
                    exception
            );
        } catch (RuntimeException exception) {
            log.warn(
                    "Could not sample Evelyn Price Index: instance={}, path={}, tradingPair={}, samplingInstant={}",
                    instanceName,
                    statusHistoryPath,
                    eveUsdtTradingPair,
                    samplingInstant,
                    exception
            );
        }
    }

    private void persistAndPublish(EvelynStatusIndexPoint point) throws IOException {
        synchronized (statusIndexHistory) {
            if (!isStatusIndexSamplingActive()) {
                return;
            }
            if (lastPersistedTimestamp != null
                    && point.timestamp().isBefore(lastPersistedTimestamp)) {
                throw new IllegalStateException(
                        "Sample timestamp " + point.timestamp()
                                + " is earlier than last persisted timestamp "
                                + lastPersistedTimestamp
                );
            }

            appendStatusRecord(point);
            lastPersistedTimestamp = point.timestamp();
            statusIndexHistory.add(point);
        }
    }

    private void appendStatusRecord(EvelynStatusIndexPoint point) throws IOException {
        String encodedRecord = STATUS_TIMESTAMP_FORMATTER.format(
                LocalDateTime.ofInstant(point.timestamp(), ZoneOffset.UTC)
        ) + ":" + EVELYN_IOU_TOKEN_PRICE_INDEX_RECORD_TYPE
                + ":" + point.evelynPriceIndex().toPlainString() + "\n";

        try (FileChannel channel = FileChannel.open(
                statusHistoryPath,
                StandardOpenOption.READ,
                StandardOpenOption.WRITE
        )) {
            long size = channel.size();
            boolean needsLineSeparator = size > 0
                    && !endsWithLineSeparator(channel, size);
            channel.position(size);
            if (needsLineSeparator) {
                writeFully(channel, ByteBuffer.wrap(new byte[] {'\n'}));
            }
            writeFully(
                    channel,
                    ByteBuffer.wrap(encodedRecord.getBytes(StandardCharsets.UTF_8))
            );
            channel.force(true);
        }
    }

    private boolean endsWithLineSeparator(FileChannel channel, long size)
            throws IOException {
        ByteBuffer lastByte = ByteBuffer.allocate(1);
        channel.position(size - 1);
        if (channel.read(lastByte) != 1) {
            throw new IOException(
                    "Could not inspect final byte of Evelyn status history: "
                            + statusHistoryPath
            );
        }
        byte value = lastByte.array()[0];
        return value == '\n' || value == '\r';
    }

    private static void writeFully(FileChannel channel, ByteBuffer buffer)
            throws IOException {
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }
    }

    private static String removeComment(String line) {
        int commentStart = line.indexOf('#');
        return commentStart < 0 ? line : line.substring(0, commentStart);
    }

    private synchronized boolean isStatusIndexSamplingActive() {
        return statusIndexScheduler != null
                && lifecycleState == LifecycleState.STARTED;
    }

    private static String validateInstanceName(String instanceName) {
        Objects.requireNonNull(instanceName, "instanceName");
        String normalizedName = Normalizer.normalize(instanceName, Normalizer.Form.NFC);
        if (normalizedName.codePoints().allMatch(EvelynImpl::isWhitespace)
                || hasWhitespaceAtBoundary(normalizedName)
                || normalizedName.equals(".")
                || normalizedName.equals("..")
                || normalizedName.endsWith(".")
                || normalizedName.codePoints().anyMatch(Character::isISOControl)
                || normalizedName.codePoints().anyMatch(
                        codePoint -> PORTABLE_FILENAME_INVALID_CHARACTERS
                                .indexOf(codePoint) >= 0
                )
                || isWindowsReservedFilename(normalizedName)) {
            throw new IllegalArgumentException(
                    "Unsafe Evelyn instance name: " + normalizedName
            );
        }
        return normalizedName;
    }

    private static boolean hasWhitespaceAtBoundary(String value) {
        return isWhitespace(value.codePointAt(0))
                || isWhitespace(value.codePointBefore(value.length()));
    }

    private static boolean isWhitespace(int codePoint) {
        return Character.isWhitespace(codePoint) || Character.isSpaceChar(codePoint);
    }

    private static boolean isWindowsReservedFilename(String value) {
        String upperName = value.toUpperCase(Locale.ROOT);
        int extensionSeparator = upperName.indexOf('.');
        String baseName = extensionSeparator < 0
                ? upperName
                : upperName.substring(0, extensionSeparator);
        if (baseName.equals("CON")
                || baseName.equals("PRN")
                || baseName.equals("AUX")
                || baseName.equals("NUL")) {
            return true;
        }
        if (baseName.length() == 4) {
            String prefix = baseName.substring(0, 3);
            char suffix = baseName.charAt(3);
            return (prefix.equals("COM") || prefix.equals("LPT"))
                    && suffix >= '1' && suffix <= '9';
        }
        return false;
    }

    private static final Logger log = LoggerFactory.getLogger(EvelynImpl.class);
    private static final Path DATA_ROOT = Path.of("data", "evelyn");
    private static final String STATUS_HISTORY_FILENAME = "status.log";
    private static final String FORMAT_VERSION_KEY = "FORMAT_VERSION";
    private static final int SUPPORTED_STATUS_HISTORY_FORMAT_VERSION = 1;
    private static final int EVELYN_IOU_TOKEN_PRICE_INDEX_RECORD_TYPE = 1;
    private static final int STATUS_RECORD_FIELD_COUNT = 3;
    private static final long SAMPLE_DELAY_MINUTES = 1;
    private static final long TERMINATION_TIMEOUT_SECONDS = 10;
    private static final String PORTABLE_FILENAME_INVALID_CHARACTERS = "<>:\"/\\|?*";
    private static final DateTimeFormatter STATUS_TIMESTAMP_FORMATTER =
            DateTimeFormatter.ofPattern("uuuuMMddHHmmssSSS'Z'")
                    .withResolverStyle(ResolverStyle.STRICT);
    private static final Set<String> RESERVED_INSTANCE_NAMES =
            ConcurrentHashMap.newKeySet();

    private final String instanceName;
    private final String reservationKey;
    private final Path instancePath;
    private final Path statusHistoryPath;
    private final TickerService tickerService;
    private final TradingPair eveUsdtTradingPair;
    private final Clock clock;
    private final long sampleDelay;
    private final TimeUnit sampleDelayUnit;
    private final List<EvelynStatusIndexPoint> statusIndexHistory;

    private ScheduledExecutorService statusIndexScheduler;
    private Instant lastPersistedTimestamp;
    private LifecycleState lifecycleState = LifecycleState.STOPPED;
    private boolean closeRequested;

    private enum LifecycleState {
        STOPPED,
        STARTING,
        STARTED,
        STOPPING,
        TERMINATION_FAILED,
        CLOSED
    }

    private record LoadedHistory(
            List<EvelynStatusIndexPoint> points,
            Instant lastTimestamp
    ) {
    }

    private static final class UnknownStatusRecordTypeException
            extends RuntimeException {
        private UnknownStatusRecordTypeException(int recordType) {
            this.recordType = recordType;
        }

        private final int recordType;
    }

    /*
    private SPLTokenHolding getSPLHolding(ΩSolanaAddressΩ ownerAddress, ΩSPLMintAddressΩ splMintAddress) throws Exception {
        Map<ΩSPLMintAddressΩ, SPLTokenHolding> holdings = new HashMap<>();

        holdings.putAll(solanaChain.getSPLTokenHoldings(ownerAddress, SPL_TOKEN_PROGRAM));
        holdings.putAll(solanaChain.getSPLTokenHoldings(ownerAddress, TOKEN_2022_PROGRAM));

        SPLTokenHolding result = holdings.get(splMintAddress);
        return result;
    }

    private MoneyAmount add(MoneyAmount a, ΩAmountΩ b) {
        MoneyAmount ma = new MoneyAmount(a.amount().add(b), a.currencyType());
        return ma;
    }

    private MoneyAmount add(MoneyAmount a, MoneyAmount b) {
        if(!a.currencyType().equals(b.currencyType())) {
            String errTxt = "Cannot add " + b.currencyType()
                    + " to " + a.currencyType();
            throw new IllegalArgumentException(errTxt);
        }

        MoneyAmount ma = new MoneyAmount(a.amount().add(b.amount()), a.currencyType());
        return ma;
    }

    private MoneyAmount subtract(MoneyAmount a, MoneyAmount b) {
        if(!a.currencyType().equals(b.currencyType())) {
            String errTxt = "Cannot subtract " + b.currencyType()
                    + " from " + a.currencyType();
            throw new IllegalArgumentException(errTxt);
        }

        MoneyAmount ma = new MoneyAmount(a.amount().subtract(b.amount()), a.currencyType());
        return ma;
    }

    private MoneyAmount subtract(MoneyAmount a, ΩAmountΩ b) {
        MoneyAmount ma = new MoneyAmount(a.amount().subtract(b), a.currencyType());
        return ma;
    }

    private ΩAmountΩ subtract(ΩAmountΩ a, ΩAmountΩ b) {
        return a.subtract(b);
    }

    private ΩAmountΩ subtract(ΩAmountΩ a, MoneyAmount b) {
        return a.subtract(b.amount());
    }

    private void handleRebalancingProposal() {
        try {
            ΩSolanaAddressΩ solanaAddressForEvelynIOU = state.getSolanaAddressForEvelynIOU();
            ΩSPLMintAddressΩ syrupUSDCMintAddr = SolanaConstants.SPL_TOKEN_SYRUPUSDC;

            AssetPrice currentPriceFromRaydium;
            SPLTokenHolding syrupHolding = getSPLHolding(solanaAddressForEvelynIOU, syrupUSDCMintAddr);

            ΩSyrupAmountΩ inactiveInAccountSyrup = new ΩSyrupAmountΩ(
                    syrupHolding.uiAmount(),
                    SYRUPUSDC.getCurrencyType()
            );

            while(true) {
                state.update();
                ΩmilliSecondsΩ iterationStartTime = System.currentTimeMillis();

                try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
                    RaydiumLiquidityPoolPrice concentratedResult;
                    Future<RaydiumLiquidityPoolPrice> cFuture = executor.submit(
                            () -> new RaydiumLiquidityPoolPrice(
                                    state.getRaydiumPoolId(),
                                    raydium.fetchPoolPrice(state.getRaydiumPoolId())
                            )
                    );

                    concentratedResult = cFuture.get();
                    currentPriceFromRaydium = concentratedResult.price();
                }

                System.out.println("Iteration interval: " + (state.getIterationInterval() / 1000) + " secs");

                System.out.println("Solana balances:");
                ΩSolanaAmountΩ solBalanceEvelynAccount = solanaChain.getBalanceInSolana(state.getSolanaAddressForEvelynIOU());
                System.out.println("   Evelyn account: " + solBalanceEvelynAccount.amount());
                ΩSolanaAmountΩ solBalanceBurnAccount = solanaChain.getBalanceInSolana(state.getSolanaAddressForEvelynIOUBurner());
                System.out.println("     Burn account: " + solBalanceBurnAccount.amount());
                System.out.println("SYRUP owned by Evelyn: " + state.getSyrupOwnedByEvelyn().amount());
                System.out.println("SYRUP inactive on Evelyn account: " + inactiveInAccountSyrup);

                System.out.println("Pool price: " + currentPriceFromRaydium);

                Pair<ΩSolanaAmountΩ, ΩSyrupAmountΩ> totalDistributedSums =
                        desiredPositionCalculator.calculateTotalDistributedSums();
                ΩSolanaAmountΩ totalDistributedSumSolana = totalDistributedSums.left();
                ΩSyrupAmountΩ totalDistributedSumSyrup = totalDistributedSums.right();
                System.out.println("Total amount currently distributed: " + totalDistributedSumSolana
                        + " / " + totalDistributedSumSyrup);

                Pair<ΩSolanaAmountΩ, ΩSyrupAmountΩ> amountsLocked =
                        desiredPositionCalculator.calculateLockedSums(currentPriceFromRaydium);
                ΩSolanaAmountΩ amountLockedSolana = amountsLocked.left();
                ΩSyrupAmountΩ amountLockedSyrup = amountsLocked.right();
                System.out.println("Total amount locked due to HIGH price: " + amountLockedSolana);
                System.out.println("Total amount locked due to LOW price: " + amountLockedSyrup);

                ΩSyrupAmountΩ totalAmountSyrup = add(totalDistributedSumSyrup, inactiveInAccountSyrup);
                ΩSyrupAmountΩ reservedForBurnSyrup = subtract(totalAmountSyrup, state.getSyrupOwnedByEvelyn());

                ΩSolanaAmountΩ readyForBurnSolana = subtract(solBalanceEvelynAccount, SOFT_LOW_LIMIT_SOLANA_BALANCE);
                System.out.println("Amount reserved for burn: "
                        + "Solana: " + readyForBurnSolana
                        + ", Syrup:" + reservedForBurnSyrup);

                Pair<ΩSolanaAmountΩ, ΩSyrupAmountΩ> totalReadyAmount = desiredPositionCalculator
                        .calculateRedistributableSums(
                                currentPriceFromRaydium,
                                solBalanceEvelynAccount,
                                amountZeroSolana,           // We do not burn Solana
                                inactiveInAccountSyrup,
                                reservedForBurnSyrup
                        );
                ΩSolanaAmountΩ solTotalReadyAmount = totalReadyAmount.left();
                ΩSyrupAmountΩ syrupTotalReadyAmount = totalReadyAmount.right();
                System.out.println("Total amount of Syrup ready for distribution: " + syrupTotalReadyAmount);

                HashMap<ΩRaydiumLiquidityPoolPositionNftIdΩ, Pair<ΩSolanaAmountΩ, ΩSyrupAmountΩ>> rebalancingProposal =
                        desiredPositionCalculator.calculateRebalancingProposal(
                                currentPriceFromRaydium,
                                solTotalReadyAmount,
                                syrupTotalReadyAmount
                        );

                Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityPositions =
                        state.getLiquidityPositions();

                Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, Triblet<ΩSyrupAmountΩ, ΩSyrupAmountΩ, ΩSyrupAmountΩ>> diffs =
                        calculateDiffs(rebalancingProposal, liquidityPositions);

                String minKey = diffs.entrySet().stream()
                        .min(Comparator.comparing(entry -> entry.getValue().right().amount()))
                        .map(Map.Entry::getKey)
                        .orElseThrow();

                String maxKey = diffs.entrySet().stream()
                        .max(Comparator.comparing(entry -> entry.getValue().right().amount()))
                        .map(Map.Entry::getKey)
                        .orElseThrow();

                System.out.println("Move value from (" + maxKey + "): ");

                RaydiumLiquidityPoolPositionConcentrated maxPos = liquidityPositions.get(maxKey);
                Triblet<ΩSyrupAmountΩ, ΩSyrupAmountΩ, ΩSyrupAmountΩ> maxDiff = diffs.get(maxKey);
                System.out.println("   " + maxPos.priceRange().from() + ".."
                        + maxPos.priceRange().to()
                        + " - Current: " + maxDiff.left().amount()
                        + ", Suggested: " + maxDiff.middle().amount()
                        + ", Diff: " + maxDiff.right().amount()
                );

                System.out.println(" --> ");

                RaydiumLiquidityPoolPositionConcentrated minPos = liquidityPositions.get(minKey);
                Triblet<ΩSyrupAmountΩ, ΩSyrupAmountΩ, ΩSyrupAmountΩ> minDiff = diffs.get(minKey);
                System.out.println("   " + minPos.priceRange().from() + ".."
                        + minPos.priceRange().to()
                        + " - Current: " + minDiff.left().amount()
                        + ", Suggested: " + minDiff.middle().amount()
                        + ", Diff: " + minDiff.right().amount());

                ΩmilliSecondsΩ iterationExecutionTime = (System.currentTimeMillis() - iterationStartTime);

                for(ΩmilliSecondsΩ sleepTime = state.getIterationInterval() - iterationExecutionTime; sleepTime > 0; sleepTime -= 1000) {
                    System.out.print("\rSleeping for " + (sleepTime / 1000) + " secs...      ");
                    System.out.flush();
                    Thread.sleep(1000L);
                }
                System.out.println();
                System.out.println("-------------------------------------------------------------");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private HashMap<ΩRaydiumLiquidityPoolPositionNftIdΩ, Triblet<ΩSyrupAmountΩ, ΩSyrupAmountΩ, ΩSyrupAmountΩ>> calculateDiffs(
            Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, Pair<ΩSolanaAmountΩ, ΩSyrupAmountΩ>> rebalancingProposal,
            Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityProviderPositionMap
    ) {
        HashMap<ΩRaydiumLiquidityPoolPositionNftIdΩ, Triblet<ΩSyrupAmountΩ, ΩSyrupAmountΩ, ΩSyrupAmountΩ>> diffs =
                new HashMap<>(rebalancingProposal.size());

        for(ΩRaydiumLiquidityPoolPositionNftIdΩ nftId : rebalancingProposal.keySet()) {
            RaydiumLiquidityPoolPositionConcentrated pos = liquidityProviderPositionMap.get(nftId);
            ΩSyrupAmountΩ currentlyAmountAdded = pos.amountMintB();
            ΩSyrupAmountΩ suggestedAmount = rebalancingProposal.get(nftId).right();
            ΩSyrupAmountΩ diff = subtract(currentlyAmountAdded, suggestedAmount);
            Triblet<ΩSyrupAmountΩ, ΩSyrupAmountΩ, ΩSyrupAmountΩ> t = new Triblet<>(
                    currentlyAmountAdded,
                    suggestedAmount,
                    diff
            );
            diffs.put(nftId, t);
        }

        return diffs;
    }

    private final static ΩSolanaAmountΩ amountZeroSolana = new ΩSolanaAmountΩ(
            BigDecimal.ZERO,
            SOLANA.getCurrencyType()
    );

    private final ΩAmountΩ SOFT_LOW_LIMIT_SOLANA_BALANCE = new BigDecimal("0.1");

    private final State state;
    private final DesiredPositionCalculator desiredPositionCalculator;
    private final Raydium raydium;
    private final SolanaBlockChain solanaChain;
    */
}
