import "mainnet-hub-types"
import "mainnet-position-types"

contract HalvoraMainnetPosition {
    author: "Halvora"
    version: "1.0.0-mainnet-candidate"
    description: "USDT Position accounting for the Halvora mainnet candidate"
    storage: MainnetPositionStorage
    incomingMessages: MainnetPositionMessage
}

type MainnetPositionMessage =
    | MainnetCreditPositionDeposit
    | RequestMainnetClaim
    | RequestMainnetFullReinvest
    | ConfirmMainnetPayout
    | MainnetClaimIntentAccepted
    | RetryMainnetClaimIntent
    | ApplyMainnetPositionUpgrade

fun minimum(left: int, right: int): int {
    return left < right ? left : right;
}

fun floorToHour(timestamp: int): int {
    return timestamp - timestamp % HOUR_SECONDS;
}

fun totalTierPower(tier: MainnetTierAccounting): coins {
    return tier.activePower + tier.pendingPower;
}

fun accrueTier(tier: MainnetTierAccounting, rate: int, hours: int): MainnetTierAccounting {
    if (hours <= 0 || tier.activePower == 0) {
        return tier;
    }

    val numerator = tier.activePower * rate * hours + tier.remainder;
    tier.accrued += numerator / (BASIS_POINTS * HOURS_PER_DAY);
    tier.remainder = numerator % (BASIS_POINTS * HOURS_PER_DAY);
    return tier;
}

fun MainnetPositionStorage.settle(mutate self, timestamp: int): void {
    assert (timestamp >= self.lastAccrualAt) throw MainnetPositionErrors.InvalidTimestamp;

    val target = floorToHour(timestamp);
    if (target <= self.lastAccrualAt) {
        return;
    }

    var tier3 = self.tier3.load();
    var tier4 = self.tier4.load();
    var tier5 = self.tier5.load();

    if (self.pendingActivationAt != 0 && self.pendingActivationAt <= target) {
        val hoursBeforeActivation = (self.pendingActivationAt - self.lastAccrualAt) / HOUR_SECONDS;
        tier3 = accrueTier(tier3, RATE_3_PERCENT, hoursBeforeActivation);
        tier4 = accrueTier(tier4, RATE_4_PERCENT, hoursBeforeActivation);
        tier5 = accrueTier(tier5, RATE_5_PERCENT, hoursBeforeActivation);

        tier3.activePower += tier3.pendingPower;
        tier4.activePower += tier4.pendingPower;
        tier5.activePower += tier5.pendingPower;
        tier3.pendingPower = 0;
        tier4.pendingPower = 0;
        tier5.pendingPower = 0;

        val hoursAfterActivation = (target - self.pendingActivationAt) / HOUR_SECONDS;
        tier3 = accrueTier(tier3, RATE_3_PERCENT, hoursAfterActivation);
        tier4 = accrueTier(tier4, RATE_4_PERCENT, hoursAfterActivation);
        tier5 = accrueTier(tier5, RATE_5_PERCENT, hoursAfterActivation);
        self.pendingActivationAt = 0;
    } else {
        val hours = (target - self.lastAccrualAt) / HOUR_SECONDS;
        tier3 = accrueTier(tier3, RATE_3_PERCENT, hours);
        tier4 = accrueTier(tier4, RATE_4_PERCENT, hours);
        tier5 = accrueTier(tier5, RATE_5_PERCENT, hours);
    }

    self.lastAccrualAt = target;
    self.tier3 = tier3.toCell();
    self.tier4 = tier4.toCell();
    self.tier5 = tier5.toCell();
}

fun MainnetPositionStorage.addPower(
    mutate self,
    mutate tier: MainnetTierAccounting,
    amount: coins,
    timestamp: int,
): void {
    if (amount == 0) {
        return;
    }
    if (timestamp % HOUR_SECONDS == 0) {
        tier.activePower += amount;
        return;
    }

    val activationAt = floorToHour(timestamp) + HOUR_SECONDS;
    if (self.pendingActivationAt == 0) {
        self.pendingActivationAt = activationAt;
    }
    assert (self.pendingActivationAt == activationAt) throw MainnetPositionErrors.InvalidTimestamp;
    tier.pendingPower += amount;
}

fun MainnetPositionStorage.resetCycle(mutate self, timestamp: int): void {
    self.cycleId += 1;
    self.completed = false;
    self.lastAccrualAt = floorToHour(timestamp);
    self.pendingActivationAt = 0;
    self.lastClaimAt = timestamp;
    self.tier3 = emptyTier().toCell();
    self.tier4 = emptyTier().toCell();
    self.tier5 = emptyTier().toCell();
    self.totals = emptyTotals().toCell();
}

fun depositRate(amount: coins): int {
    if (amount >= 5_000 * MICRO_USDT) {
        return RATE_5_PERCENT;
    }
    if (amount >= 3_000 * MICRO_USDT) {
        return RATE_4_PERCENT;
    }
    return RATE_3_PERCENT;
}

fun MainnetPositionStorage.totalPower(self): coins {
    return totalTierPower(self.tier3.load()) + totalTierPower(self.tier4.load()) +
    totalTierPower(self.tier5.load());
}

fun sendClaimIntent(
    storage: MainnetPositionStorage,
    positionQueryId: uint64,
    pending: MainnetPendingPositionClaim,
): void {
    val intent = createMessage({
        bounce: BounceMode.RichBounce,
        value: ton("0.02"),
        dest: storage.hub,
        body: MainnetPositionClaimIntent {
            positionQueryId,
            cycleId: pending.cycleId,
            owner: storage.owner,
            amount: pending.amount,
        },
    });
    intent.send(SEND_MODE_BOUNCE_ON_ACTION_FAIL);

    // Messages emitted by the same account to the same destination are
    // delivered in generation order. The Hub records the claim before this
    // best-effort dispatch. If dispatch cannot start, the recorded claim stays
    // in the FIFO queue for a later retry.
    val dispatch = createMessage({
        bounce: BounceMode.RichBounce,
        value: MAINNET_AUTOMATED_DISPATCH_GRAM,
        dest: storage.hub,
        body: DispatchMainnetPayout {},
    });
    dispatch.send(SEND_MODE_BOUNCE_ON_ACTION_FAIL);
}

fun sendPayoutRecorded(storage: MainnetPositionStorage, queueId: uint64, amount: coins): void {
    val acknowledgment = createMessage({
        bounce: BounceMode.NoBounce,
        value: ton("0.02"),
        dest: storage.hub,
        body: MainnetPositionPayoutRecorded {
            queueId,
            owner: storage.owner,
            amount,
        },
    });
    acknowledgment.send(SEND_MODE_REGULAR);
}

fun onInternalMessage(in: InMessage) {
    val msg = lazy MainnetPositionMessage.fromSlice(in.body);

    match (msg) {
        ApplyMainnetPositionUpgrade => {
            val storage = lazy MainnetPositionStorage.load();
            assert (in.senderAddress == storage.hub) throw MainnetPositionErrors.NotHub;
            contract.setCodePostponed(msg.newCode);
        }
        MainnetCreditPositionDeposit => {
            var storage = lazy MainnetPositionStorage.load();
            assert (in.senderAddress == storage.hub) throw MainnetPositionErrors.NotHub;
            assert (
                msg.amount >= MINIMUM_DEPOSIT && msg.amount <= MAXIMUM_EXTERNAL_DEPOSITS
            ) throw MainnetPositionErrors.InvalidDeposit;
            assert (
                msg.queryId > storage.lastDepositQueryId
            ) throw MainnetPositionErrors.DuplicateQuery;

            var totals = storage.totals.load();
            if (storage.completed) {
                assert (
                    totals.walletPaid == totals.walletCommitted
                ) throw MainnetPositionErrors.PreviousCycleUnpaid;
                storage.resetCycle(blockchain.now());
                totals = storage.totals.load();
            }

            val timestamp = blockchain.now();
            storage.settle(timestamp);
            assert (
                totals.externalDeposits + msg.amount <= MAXIMUM_EXTERNAL_DEPOSITS
            ) throw MainnetPositionErrors.ExternalDepositCap;
            assert (
                storage.totalPower() + msg.amount <= MAXIMUM_MINING_POWER
            ) throw MainnetPositionErrors.MiningPowerCap;

            val rate = depositRate(msg.amount);
            if (rate == RATE_3_PERCENT) {
                var tier = storage.tier3.load();
                storage.addPower(mutate tier, msg.amount, timestamp);
                storage.tier3 = tier.toCell();
            } else if (rate == RATE_4_PERCENT) {
                var tier = storage.tier4.load();
                storage.addPower(mutate tier, msg.amount, timestamp);
                storage.tier4 = tier.toCell();
            } else {
                var tier = storage.tier5.load();
                storage.addPower(mutate tier, msg.amount, timestamp);
                storage.tier5 = tier.toCell();
            }

            if (totals.externalDeposits == 0) {
                storage.lastClaimAt = timestamp;
            }
            totals.externalDeposits += msg.amount;
            totals.payoutCap += msg.amount * PAYOUT_CAP_MULTIPLIER;
            storage.lastDepositQueryId = msg.queryId;
            storage.totals = totals.toCell();
            storage.save();

            val acceptedMessage = createMessage({
                bounce: BounceMode.NoBounce,
                value: ton("0.02"),
                dest: storage.hub,
                body: MainnetPositionDepositAccepted {
                    queryId: msg.queryId,
                    owner: storage.owner,
                    amount: msg.amount,
                },
            });
            acceptedMessage.send(SEND_MODE_REGULAR);
        }
        RequestMainnetClaim => {
            var storage = lazy MainnetPositionStorage.load();
            assert (in.senderAddress == storage.owner) throw MainnetPositionErrors.NotOwner;
            assert (
                in.valueCoins >= MAINNET_AUTOMATED_CLAIM_GRAM
            ) throw MainnetPositionErrors.InsufficientClaimGram;
            assert (!storage.completed) throw MainnetPositionErrors.PositionCompleted;

            val timestamp = blockchain.now();
            storage.settle(timestamp);
            assert (
                timestamp - storage.lastClaimAt >= CLAIM_COOLDOWN_SECONDS
            ) throw MainnetPositionErrors.ClaimCooldown;

            var tier3 = storage.tier3.load();
            var tier4 = storage.tier4.load();
            var tier5 = storage.tier5.load();
            var totals = storage.totals.load();

            val totalAccrued = tier3.accrued + tier4.accrued + tier5.accrued;
            assert (totalAccrued > 0) throw MainnetPositionErrors.NothingToClaim;

            var availablePower = MAXIMUM_MINING_POWER - totalTierPower(tier3) -
            totalTierPower(tier4) -
            totalTierPower(tier5);
            val wanted3 = tier3.accrued * REINVEST_BASIS_POINTS / BASIS_POINTS;
            val reinvest3 = minimum(wanted3, availablePower);
            availablePower -= reinvest3;
            val wanted4 = tier4.accrued * REINVEST_BASIS_POINTS / BASIS_POINTS;
            val reinvest4 = minimum(wanted4, availablePower);
            availablePower -= reinvest4;
            val wanted5 = tier5.accrued * REINVEST_BASIS_POINTS / BASIS_POINTS;
            val reinvest5 = minimum(wanted5, availablePower);

            val totalReinvest = reinvest3 + reinvest4 + reinvest5;
            val plannedWalletAmount = totalAccrued - totalReinvest;
            val remainingCap = totals.payoutCap - totals.walletCommitted;
            assert (remainingCap > 0) throw MainnetPositionErrors.PayoutCapReached;

            val closesCycle = plannedWalletAmount >= remainingCap;
            val walletAmount = closesCycle ? remainingCap : plannedWalletAmount;
            assert (
                closesCycle || walletAmount >= MINIMUM_NET_CLAIM
            ) throw MainnetPositionErrors.BelowMinimumClaim;

            if (closesCycle) {
                totals.discardedAccrual += totalAccrued - walletAmount;
                storage.completed = true;
                storage.pendingActivationAt = 0;
                tier3 = emptyTier();
                tier4 = emptyTier();
                tier5 = emptyTier();
            } else {
                tier3.accrued = 0;
                tier4.accrued = 0;
                tier5.accrued = 0;
                storage.addPower(mutate tier3, reinvest3, timestamp);
                storage.addPower(mutate tier4, reinvest4, timestamp);
                storage.addPower(mutate tier5, reinvest5, timestamp);
                totals.totalReinvested += totalReinvest;
            }

            totals.walletCommitted += walletAmount;
            var claims = totals.claims.load();
            claims.nextIntentId += 1;
            val positionQueryId = claims.nextIntentId;
            val pending = MainnetPendingPositionClaim {
                cycleId: storage.cycleId,
                amount: walletAmount,
            };
            claims.pendingByQuery.set(positionQueryId, pending);
            claims.pendingCount += 1;
            claims.pendingTotal += walletAmount;
            storage.lastClaimAt = timestamp;
            totals.claims = claims.toCell();
            storage.tier3 = tier3.toCell();
            storage.tier4 = tier4.toCell();
            storage.tier5 = tier5.toCell();
            storage.totals = totals.toCell();
            storage.save();
            sendClaimIntent(storage, positionQueryId, pending);
        }
        RequestMainnetFullReinvest => {
            var storage = lazy MainnetPositionStorage.load();
            assert (in.senderAddress == storage.owner) throw MainnetPositionErrors.NotOwner;
            assert (!storage.completed) throw MainnetPositionErrors.PositionCompleted;

            val timestamp = blockchain.now();
            storage.settle(timestamp);

            var tier3 = storage.tier3.load();
            var tier4 = storage.tier4.load();
            var tier5 = storage.tier5.load();
            var totals = storage.totals.load();
            var availablePower = MAXIMUM_MINING_POWER - totalTierPower(tier3) -
            totalTierPower(tier4) -
            totalTierPower(tier5);
            assert (availablePower > 0) throw MainnetPositionErrors.MiningPowerCap;

            val reinvest3 = minimum(tier3.accrued, availablePower);
            availablePower -= reinvest3;
            val reinvest4 = minimum(tier4.accrued, availablePower);
            availablePower -= reinvest4;
            val reinvest5 = minimum(tier5.accrued, availablePower);
            val totalReinvest = reinvest3 + reinvest4 + reinvest5;
            assert (totalReinvest > 0) throw MainnetPositionErrors.NothingToReinvest;

            tier3.accrued -= reinvest3;
            tier4.accrued -= reinvest4;
            tier5.accrued -= reinvest5;
            storage.addPower(mutate tier3, reinvest3, timestamp);
            storage.addPower(mutate tier4, reinvest4, timestamp);
            storage.addPower(mutate tier5, reinvest5, timestamp);
            totals.totalReinvested += totalReinvest;

            storage.tier3 = tier3.toCell();
            storage.tier4 = tier4.toCell();
            storage.tier5 = tier5.toCell();
            storage.totals = totals.toCell();
            storage.save();
        }
        MainnetClaimIntentAccepted => {
            var storage = lazy MainnetPositionStorage.load();
            assert (in.senderAddress == storage.hub) throw MainnetPositionErrors.NotHub;
            assert (
                msg.owner == storage.owner
            ) throw MainnetPositionErrors.InvalidClaimAcknowledgment;
            var totals = storage.totals.load();
            var claims = totals.claims.load();

            if (claims.pendingByQuery.exists(msg.positionQueryId)) {
                val pending = claims.pendingByQuery.mustGet(msg.positionQueryId);
                assert (
                    msg.cycleId == pending.cycleId && msg.amount == pending.amount
                ) throw MainnetPositionErrors.InvalidClaimAcknowledgment;
                claims.pendingByQuery.delete(msg.positionQueryId);
                claims.pendingCount -= 1;
                claims.pendingTotal -= pending.amount;
                totals.claims = claims.toCell();
                storage.totals = totals.toCell();
                storage.save();
            } else {
                assert (
                    msg.positionQueryId <= claims.nextIntentId
                ) throw MainnetPositionErrors.InvalidClaimAcknowledgment;
            }
        }
        RetryMainnetClaimIntent => {
            val storage = lazy MainnetPositionStorage.load();
            assert (in.senderAddress == storage.owner) throw MainnetPositionErrors.NotOwner;
            assert (
                in.valueCoins >= MAINNET_AUTOMATED_CLAIM_GRAM
            ) throw MainnetPositionErrors.InsufficientClaimGram;
            val claims = storage.totals.load().claims.load();
            assert (
                claims.pendingByQuery.exists(msg.positionQueryId)
            ) throw MainnetPositionErrors.PendingClaimNotFound;
            val pending = claims.pendingByQuery.mustGet(msg.positionQueryId);
            sendClaimIntent(storage, msg.positionQueryId, pending);
        }
        ConfirmMainnetPayout => {
            var storage = lazy MainnetPositionStorage.load();
            assert (in.senderAddress == storage.hub) throw MainnetPositionErrors.NotHub;
            assert (msg.amount > 0) throw MainnetPositionErrors.InvalidPayout;
            var totals = storage.totals.load();
            var claims = totals.claims.load();

            if (msg.queryId == storage.lastPayoutQueryId) {
                assert (
                    msg.amount == claims.lastPayoutAmount
                ) throw MainnetPositionErrors.InvalidPayout;
                sendPayoutRecorded(storage, msg.queryId, msg.amount);
                return;
            }
            assert (
                msg.queryId > storage.lastPayoutQueryId
            ) throw MainnetPositionErrors.DuplicateQuery;

            assert (
                totals.walletPaid + msg.amount <= totals.walletCommitted
            ) throw MainnetPositionErrors.InvalidPayout;
            totals.walletPaid += msg.amount;
            storage.lastPayoutQueryId = msg.queryId;
            claims.lastPayoutAmount = msg.amount;
            totals.claims = claims.toCell();
            storage.totals = totals.toCell();
            storage.save();
            sendPayoutRecorded(storage, msg.queryId, msg.amount);
        }
        else => {
            assert (in.body.isEmpty()) throw MainnetPositionErrors.InvalidPositionMessage;
        }
    }
}

fun onBouncedMessage(_in: InMessageBounced) {}

get fun positionHub(): address {
    return MainnetPositionStorage.load().hub;
}

get fun positionOwner(): address {
    return MainnetPositionStorage.load().owner;
}

get fun cycleId(): int {
    return MainnetPositionStorage.load().cycleId;
}

get fun completed(): bool {
    return MainnetPositionStorage.load().completed;
}

get fun externalDeposits(): coins {
    return MainnetPositionStorage.load().totals.load().externalDeposits;
}

get fun payoutCap(): coins {
    return MainnetPositionStorage.load().totals.load().payoutCap;
}

get fun walletCommitted(): coins {
    return MainnetPositionStorage.load().totals.load().walletCommitted;
}

get fun walletPaid(): coins {
    return MainnetPositionStorage.load().totals.load().walletPaid;
}

get fun totalReinvested(): coins {
    return MainnetPositionStorage.load().totals.load().totalReinvested;
}

get fun discardedAccrual(): coins {
    return MainnetPositionStorage.load().totals.load().discardedAccrual;
}

get fun pendingClaimIntents(): int {
    return MainnetPositionStorage.load().totals.load().claims.load().pendingCount;
}

get fun pendingClaimAmount(): coins {
    return MainnetPositionStorage.load().totals.load().claims.load().pendingTotal;
}

get fun miningPower(): coins {
    var storage = MainnetPositionStorage.load();
    storage.settle(blockchain.now());
    return storage.totalPower();
}

get fun accruedRewards(): coins {
    var storage = MainnetPositionStorage.load();
    storage.settle(blockchain.now());
    return storage.tier3.load().accrued + storage.tier4.load().accrued +
    storage.tier5.load().accrued;
}

get fun tier3Power(): coins {
    var storage = MainnetPositionStorage.load();
    storage.settle(blockchain.now());
    return totalTierPower(storage.tier3.load());
}

get fun tier4Power(): coins {
    var storage = MainnetPositionStorage.load();
    storage.settle(blockchain.now());
    return totalTierPower(storage.tier4.load());
}

get fun tier5Power(): coins {
    var storage = MainnetPositionStorage.load();
    storage.settle(blockchain.now());
    return totalTierPower(storage.tier5.load());
}
