// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20Metadata} from "@openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";

import {NoteKeeper} from "./types/NoteKeeper.sol";
import {IBondDepository} from "./interfaces/IBondDepository.sol";
import {IWETH9} from "./interfaces/IWETH9.sol";
import {FullMath} from "./libraries/FullMath.sol";

contract GhostBondDepository is IBondDepository, NoteKeeper {
    using SafeERC20 for IERC20;

    address public immutable WETH;

    Market[] public markets;
    Term[] public terms;
    Metadata[] public metadatas;

    mapping(uint256 => Adjustment) public adjustments;
    mapping(address => uint256[]) public marketsForQuote;

    constructor(
        address authority,
        address ftso,
        address ghst,
        address staking,
        address treasury,
        address wrapped
    ) NoteKeeper(authority, ftso, ghst, staking, treasury) {
        _ftso.approve(staking, type(uint256).max);
        WETH = wrapped;
    }

    function deposit(
        uint256 id,
        uint256 amount,
        uint256 maxPrice,
        address user,
        address referral
    )
        external
        override
        payable
        returns (
            uint256 payout,
            uint256 expiry,
            uint256 index
        )
    {
        Market storage market = markets[id];
        Term memory term = terms[id];
        uint48 currentTime = uint48(block.timestamp);

        if (currentTime >= term.conclusion) revert DepositoryConcluded(term.conclusion);
        _decay(id, currentTime);

        uint256 price = _marketPrice(id);
        if (price > maxPrice) revert DepositoryMaxPrice(price, maxPrice);

        expiry = term.fixedTerm ? term.vesting + currentTime : term.vesting;
        payout = FullMath.mulDiv(amount, 1e18, price) / (10**metadatas[id].quoteDecimals);
        if (payout > market.maxPayout) revert DepositoryMaxSize(payout, market.maxPayout);

        market.capacity -= market.capacityInQuote ? amount : payout;
        market.purchased += amount;
        market.sold += uint64(payout);      // forge-lint: disable-line(unsafe-typecast)
        market.totalDebt += uint64(payout); // forge-lint: disable-line(unsafe-typecast)

        emit Bond(id, amount, price);

        index = addNote(user, payout, uint48(expiry), uint48(id), referral); // forge-lint: disable-line(unsafe-typecast)

        IWETH9(WETH).deposit{value: msg.value}();
        IERC20(WETH).safeTransfer(address(_treasury), msg.value);
        if (msg.value < amount || market.quoteToken != WETH) {
            IERC20(market.quoteToken).safeTransferFrom(msg.sender, address(_treasury), amount);
        }

        if (term.maxDebt < market.totalDebt) {
            market.capacity = 0;
            emit MarketClosed(id);
        } else {
            _tune(id, currentTime);
        }
    }

    function _decay(uint256 id, uint48 time) internal {
        markets[id].totalDebt -= debtDecay(id);
        metadatas[id].lastDecay = time;

        if (adjustments[id].active) {
            Adjustment storage adjustment = adjustments[id];

            (uint64 adjustBy, uint48 secondsSince, bool stillActive) = _controlDecay(id);
            terms[id].controlVariable -= adjustBy;

            if (stillActive) {
                adjustment.change -= adjustBy;
                adjustment.timeToAdjusted -= secondsSince;
                adjustment.lastAdjustment = time;
            } else {
                adjustment.active = false;
            }
        }
    }

    function _tune(uint256 id, uint48 time) internal {
        Metadata memory meta = metadatas[id];

        if (time >= meta.lastTune + meta.tuneInterval) {
            Market memory market = markets[id];

            uint256 timeRemaining = terms[id].conclusion - time;
            uint256 price = _marketPrice(id);

            uint256 capacity = market.capacityInQuote
                ? FullMath.mulDiv(market.capacity, 1e18, price) / (10**meta.quoteDecimals)
                : market.capacity;

            // forge-lint: disable-next-line(unsafe-typecast)
            markets[id].maxPayout = uint64(FullMath.mulDiv(capacity, meta.depositInterval, timeRemaining));
            uint256 targetDebt = FullMath.mulDiv(capacity, meta.length, timeRemaining);
            // forge-lint: disable-next-line(unsafe-typecast)
            uint64 newControlVariable = uint64(FullMath.mulDiv(price, _treasury.baseSupply(), targetDebt));

            emit Tuned(id, terms[id].controlVariable, newControlVariable);

            if (newControlVariable >= terms[id].controlVariable) {
                terms[id].controlVariable = newControlVariable;
            } else {
                uint64 change = terms[id].controlVariable - newControlVariable;
                adjustments[id] = Adjustment({
                    change: change,
                    lastAdjustment: time,
                    timeToAdjusted: meta.tuneInterval,
                    active: true
                });
            }
            metadatas[id].lastTune = time;
        }
    }

    function create(
        uint256[3] calldata _market,
        uint256[2] calldata _terms,
        address _quoteToken,
        uint32[2] calldata _intervals,
        bool[2] calldata _booleans
    ) external override returns (uint256 id) {
        if (
            msg.sender != authority.governor() &&
            msg.sender != authority.policy()
        ) revert NotGuardianOrPolicy();

        uint256 secondsToConclusion = _terms[1] - block.timestamp;
        uint256 decimals = IERC20Metadata(_quoteToken).decimals();
        uint64 targetDebt = uint64(_booleans[0]
            ? FullMath.mulDiv(_market[0], 1e18, _market[1]) / 10**decimals
            : _market[0]);

        // forge-lint: disable-next-line(unsafe-typecast)
        uint64 maxPayout = uint64(FullMath.mulDiv(targetDebt, _intervals[0], secondsToConclusion));
        uint256 maxDebt = targetDebt + FullMath.mulDiv(targetDebt, _market[2], 1e5);
        uint256 controlVariable = FullMath.mulDiv(_market[1], _treasury.baseSupply(), targetDebt);

        id = markets.length;

        markets.push(
            Market({
                quoteToken: _quoteToken,
                capacityInQuote: _booleans[0],
                capacity: _market[0],
                totalDebt: targetDebt,
                maxPayout: maxPayout,
                purchased: 0,
                sold: 0
            })
        );

        terms.push(
            Term({
                fixedTerm: _booleans[1],
                controlVariable: uint64(controlVariable),   // forge-lint: disable-line(unsafe-typecast)
                vesting: uint48(_terms[0]),                 // forge-lint: disable-line(unsafe-typecast)
                conclusion: uint48(_terms[1]),              // forge-lint: disable-line(unsafe-typecast)
                maxDebt: uint64(maxDebt)                    // forge-lint: disable-line(unsafe-typecast)
            })
        );

        metadatas.push(
            Metadata({
                lastTune: uint48(block.timestamp),
                lastDecay: uint48(block.timestamp),
                // forge-lint: disable-next-line(unsafe-typecast)
                length: uint48(secondsToConclusion),
                depositInterval: _intervals[0],
                tuneInterval: _intervals[1],
                // forge-lint: disable-next-line(unsafe-typecast)
                quoteDecimals: uint8(decimals)
            })
        );

        marketsForQuote[_quoteToken].push(id);
        emit MarketCreated(id, address(_ftso), _quoteToken, _market[1]);
    }

    function close(uint256 id) external override {
        if (
            msg.sender != authority.governor() &&
            msg.sender != authority.policy()
        ) revert NotGuardianOrPolicy();

        terms[id].conclusion = uint48(block.timestamp);
        markets[id].capacity = 0;
        emit MarketClosed(id);
    }

    function marketPrice(uint256 id) public view override returns (uint256) {
        return FullMath.mulDiv(currentControlVariable(id), debtRatio(id), 10**metadatas[id].quoteDecimals);
    }

    function payoutFor(
        uint256 id,
        uint256 amount
    ) external view override returns (uint256) {
        uint256 nominator = FullMath.mulDiv(amount, 1e18, marketPrice(id));
        return nominator / 10**metadatas[id].quoteDecimals;
    }

    function debtRatio(uint256 id) public view override returns (uint256) {
        return FullMath.mulDiv(currentDebt(id), 10**metadatas[id].quoteDecimals, _treasury.baseSupply());
    }

    function currentDebt(uint256 id) public view override returns (uint256) {
        return markets[id].totalDebt - debtDecay(id);
    }

    function debtDecay(uint256 id) public view override returns (uint64) {
        Metadata memory meta = metadatas[id];
        uint256 secondsSince = block.timestamp - meta.lastDecay;
        return uint64(FullMath.mulDiv(markets[id].totalDebt, secondsSince, meta.length));
    }

    function currentControlVariable(uint256 id) public view override returns (uint256) {
        (uint64 decay, , ) = _controlDecay(id);
        return terms[id].controlVariable - decay;
    }

    function isLive(uint256 id) public view override returns (bool) {
        return terms[id].conclusion > block.timestamp;
    }

    function liveMarkets() external view override returns (uint256[] memory) {
        uint256 number;
        uint256 i;
        uint256 j;
        for (; i < markets.length; ) {
            if (isLive(i)) {
                unchecked { ++number; }
            }
            unchecked { ++i; }
        }

        uint256[] memory ids = new uint256[](number);
        i = 0;

        for (; i < markets.length; ) {
            if (isLive(i)) {
                ids[j] = i;
                unchecked { ++j; }
            }
            unchecked { ++i; }
        }
        return ids;
    }

    function liveMarketsFor(address token) external view override returns (uint256[] memory) {
        uint256[] memory mkts = marketsForQuote[token];
        uint256 number;
        uint256 i;
        uint256 j;

        for (; i < mkts.length; ) {
            if (isLive(mkts[i])) {
                unchecked { ++number; }
            }
            unchecked { ++i; }
        }

        uint256[] memory ids = new uint256[](number);
        i = 0;

        for (; i < mkts.length; ) {
            if (isLive(mkts[i])) {
                ids[j] = mkts[i];
                unchecked { ++j; }
            }
            unchecked { ++i; }
        }

        return ids;
    }

    function _marketPrice(uint256 _id) internal view returns (uint256) {
        return FullMath.mulDiv(terms[_id].controlVariable, _debtRatio(_id), 10**metadatas[_id].quoteDecimals);
    }

    function _debtRatio(uint256 id) internal view returns (uint256) {
        return FullMath.mulDiv(markets[id].totalDebt, 10**metadatas[id].quoteDecimals, _treasury.baseSupply());
    }

    function _controlDecay(uint256 id)
        internal
        view
        returns (
            uint64 decay,
            uint48 secondsSince,
            bool active
        )
    {
        Adjustment memory info = adjustments[id];
        if (!info.active) return (0, 0, false);

        secondsSince = uint48(block.timestamp) - info.lastAdjustment;

        active = secondsSince < info.timeToAdjusted;
        decay = active
            // forge-lint: disable-next-line(unsafe-typecast)
            ? uint64(FullMath.mulDiv(info.change, secondsSince, info.timeToAdjusted))
            : info.change;
    }
}
