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

// =============================================================
//                       INTERFACES
// =============================================================

interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function approve(address spender, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IERC20Metadata is IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
}

// Custom interface for IntelliGold token
interface IIntelliGoldToken is IERC20Metadata {
    function mint(address to, uint256 amount) external;
    function burn(uint256 amount) external;
}

interface AggregatorV3Interface {
    function decimals() external view returns (uint8);
    function description() external view returns (string memory);
    function version() external view returns (uint256);
    function getRoundData(uint80 _roundId) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
    function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}

// =============================================================
//                   LIBRARIES & ABSTRACTS
// =============================================================

library SafeERC20 {
    error SafeERC20FailedOperation(address token);
    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
    }
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        (bool success, bytes memory returndata) = address(token).call(data);
        if (!success) {
            if (returndata.length > 0) {
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert SafeERC20FailedOperation(address(token));
            }
        }
        if (returndata.length > 0) {
            require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
        }
    }
}

abstract contract Context {
    function _msgSender() internal view virtual returns (address) { return msg.sender; }
}

abstract contract Pausable is Context {
    event Paused(address account);
    event Unpaused(address account);
    bool private _paused;
    modifier whenNotPaused() { require(!paused(), "Pausable: paused"); _; }
    function paused() public view virtual returns (bool) { return _paused; }
    function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); }
    function _unpause() internal virtual { require(paused(), "Pausable: not paused"); _paused = false; emit Unpaused(_msgSender()); }
}

abstract contract ReentrancyGuard {
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status;
    modifier nonReentrant() {
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
    constructor() { _status = _NOT_ENTERED; }
}

// =============================================================
//                  INTELLIGOLD OTC RFQ CONTRACT
// =============================================================

contract IntelliGoldOTC_RFQ is Pausable, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using SafeERC20 for IIntelliGoldToken;

    // --- State Variables ---
    IIntelliGoldToken public immutable IGLD_TOKEN;
    IERC20 public immutable IUSD_TOKEN;

    address public immutable admin;
    address public treasuryAddress;
    AggregatorV3Interface public goldPriceAggregator;

    enum PriceSource { Chainlink, Manual }
    enum PositionType { Outright, Spread }

    struct Leg {
        uint256 maturity;
        uint256 entryPrice;
        uint256 amountIGLD;
        bool isLong;
    }

    struct Position {
        uint256 positionId;
        address owner;
        uint256 purchaseTimestamp;
        bool isActive;
        uint256 closeTimestamp;
        PositionType positionType;
        uint256 stopLossPrice;
        uint256 takeProfitPrice;
        Leg[] legs;
    }

    // Input struct for spreads to avoid stack too deep
    struct InputLeg {
        uint256 maturity;
        uint256 amountIGLD;
        bool isLong;
    }

    mapping(address => mapping(uint256 => Position)) public userPositions;
    mapping(address => uint256) public nextPositionIdForUser;
    mapping(address => bool) public isKeeper;

    uint256 public treasuryFeeBPS;
    uint256 public annualRateBPS;
    uint256 public minMaturityOffset;
    uint256 public maxMaturityOffset;
    uint256 public oracleMaxAge;
    PriceSource public spotSource;
    uint256 public manualSpotPrice;

    // --- Events ---
    event PositionOpened(address indexed user, uint256 indexed positionId, PositionType pType, Leg[] legs, uint256 sl, uint256 tp);
    event PositionClosed(address indexed user, uint256 indexed positionId, int256 pnlValue, address payoutToken, string reason);
    event KeeperUpdated(address indexed keeper, bool isNowKeeper);
    event AdminSettingsChanged(address indexed admin, string setting, uint256 value);
    event AdminAddressChanged(address indexed admin, string setting, address addr);

    modifier onlyAdmin() {
        require(msg.sender == admin, "IGLDOTC: Caller is not the admin");
        _;
    }

    constructor(
        address _igld,
        address _iusd,
        address _goldSpotAggregator,
        address _admin,
        address _treasury,
        uint256 _treasuryFeeBPS
    ) {
        require(_igld != address(0) && _iusd != address(0) && _goldSpotAggregator != address(0) && _admin != address(0) && _treasury != address(0), "IGLDOTC: Invalid address");
        require(_treasuryFeeBPS <= 1000, "IGLDOTC: Fee too high");

        IGLD_TOKEN = IIntelliGoldToken(_igld);
        IUSD_TOKEN = IERC20(_iusd);
        goldPriceAggregator = AggregatorV3Interface(_goldSpotAggregator);
        admin = _admin;
        treasuryAddress = _treasury;
        treasuryFeeBPS = _treasuryFeeBPS;

        minMaturityOffset = 1 days;
        maxMaturityOffset = 365 days * 5;
        oracleMaxAge = 3600; // 1 hour
        spotSource = PriceSource.Chainlink;
        annualRateBPS = 550; // 5.5%
    }

    // =============================================================
    //                      PRICING LOGIC
    // =============================================================

    function _getSpotPrice() internal view returns (uint256) {
        if (spotSource == PriceSource.Manual) {
            return manualSpotPrice;
        }
        (, int256 price,, uint256 updatedAt,) = goldPriceAggregator.latestRoundData();
        require(price > 0, "IGLDOTC: Invalid oracle price");
        
        // Stale check only if not using manual source
        if (oracleMaxAge > 0) {
            require(block.timestamp - updatedAt < oracleMaxAge, "IGLDOTC: Oracle price is stale");
        }
        
        // Chainlink usually returns 8 decimals for XAU/USD. Scale to 18.
        return uint256(price) * (10**(18 - goldPriceAggregator.decimals()));
    }

    function priceFor(uint256 maturity) public view returns (uint256) {
        uint256 spot = _getSpotPrice();
        if (maturity == 0 || maturity <= block.timestamp) {
            return spot;
        }
        uint256 timeToMaturity = maturity - block.timestamp;
        // Cost of Carry: spot * (BPS/10000) * (seconds / 365 days)
        uint256 interest = (spot * annualRateBPS * timeToMaturity) / (10000 * 365 days);
        return spot + interest;
    }

    // =============================================================
    //                     TRADING LOGIC
    // =============================================================

    /** * @dev Validates that the entry is logical (Price isn't already past SL/TP)
     * and that the user has granted enough Allowance to allow closing later.
     */
    function _validateTradeConditions(uint256 currentPrice, uint256 amountIGLD, uint256 sl, uint256 tp) internal view {
        // 1. SL/TP Logic Check for Long Position
        if (sl > 0) {
            require(sl < currentPrice, "IGLDOTC: Stop Loss must be BELOW current price for Longs");
        }
        if (tp > 0) {
            require(tp > currentPrice, "IGLDOTC: Take Profit must be ABOVE current price for Longs");
        }

        // 2. Permission Check (The "Lock")
        // We require the user to have approved the contract to spend their IGLD 
        // *before* opening the position. This guarantees the Keeper can close it later.
        uint256 allowance = IGLD_TOKEN.allowance(msg.sender, address(this));
        require(allowance >= amountIGLD, "IGLDOTC: Insufficient IGLD Allowance. Approve contract first to ensure SL/TP execution.");
    }

    function openPositionWithIUSD(uint256 maturity, uint256 amount, uint256 sl, uint256 tp, uint256 maxCost) external nonReentrant whenNotPaused {
        require(amount > 0, "IGLDOTC: Amount must be positive");
        _validateMaturity(maturity);

        // Get Future Price (Entry Price)
        uint256 entryPrice = priceFor(maturity);
        
        // Use Spot price for SL/TP validation logic (as SL/TP tracks the underlying asset usually)
        // or use entryPrice if your logic tracks the futures curve. 
        // Here we use entryPrice to ensure the SL/TP makes sense relative to what they are paying.
        _validateTradeConditions(entryPrice, amount, sl, tp);

        Leg[] memory legs = new Leg[](1);
        legs[0] = Leg({ maturity: maturity, entryPrice: entryPrice, amountIGLD: amount, isLong: true });

        // Calculate Cost in USD
        uint256 principal = (entryPrice * amount) / 1e18;
        _executeTrade(int256(principal), address(IUSD_TOKEN), maxCost, 0);

        // Mint IGLD to User
        IGLD_TOKEN.mint(msg.sender, amount);
        
        _createPosition(msg.sender, legs, PositionType.Outright, sl, tp);
    }

    function openPositionWithEth(uint256 maturity, uint256 amount, uint256 sl, uint256 tp, uint256 maxCost) external payable nonReentrant whenNotPaused {
        require(amount > 0, "IGLDOTC: Amount must be positive");
        _validateMaturity(maturity);

        uint256 entryPrice = priceFor(maturity);
        _validateTradeConditions(entryPrice, amount, sl, tp);

        Leg[] memory legs = new Leg[](1);
        legs[0] = Leg({ maturity: maturity, entryPrice: entryPrice, amountIGLD: amount, isLong: true });

        uint256 principal = (entryPrice * amount) / 1e18;
        _executeTrade(int256(principal), address(0), maxCost, 0);

        IGLD_TOKEN.mint(msg.sender, amount);
        
        _createPosition(msg.sender, legs, PositionType.Outright, sl, tp);
    }

    function openSpreadPosition(InputLeg[] calldata inputLegs, address paymentToken, uint256 maxCost, uint256 minCredit) external payable nonReentrant whenNotPaused {
        require(inputLegs.length > 1, "IGLDOTC: Spread must have multiple legs");
        
        Leg[] memory legs = new Leg[](inputLegs.length);
        int256 netCost = 0;
        uint256 totalIGLDToMint = 0;
        int256 netIGLDExposure = 0; // Ensures net long

        for (uint i = 0; i < inputLegs.length; i++) {
            InputLeg calldata inputLeg = inputLegs[i];
            require(inputLeg.amountIGLD > 0, "IGLDOTC: Leg amount positive");
            _validateMaturity(inputLeg.maturity);

            uint256 price = priceFor(inputLeg.maturity);
            legs[i] = Leg(inputLeg.maturity, price, inputLeg.amountIGLD, inputLeg.isLong);
            
            int256 legValue = int256((price * inputLeg.amountIGLD) / 1e18);

            if (inputLeg.isLong) {
                netCost += legValue;
                totalIGLDToMint += inputLeg.amountIGLD;
                netIGLDExposure += int256(inputLeg.amountIGLD);
            } else {
                netCost -= legValue;
                netIGLDExposure -= int256(inputLeg.amountIGLD);
            }
        }

        require(netIGLDExposure >= 0, "IGLDOTC: Net short positions not allowed");
        
        // For spreads, we ensure user has approved enough IGLD to cover the tokens we are about to mint them
        // This ensures we can close the position later.
        if (totalIGLDToMint > 0) {
            uint256 allowance = IGLD_TOKEN.allowance(msg.sender, address(this));
            require(allowance >= totalIGLDToMint, "IGLDOTC: Insufficient IGLD Allowance for Spread");
        }

        _executeTrade(netCost, paymentToken, maxCost, minCredit);

        if (totalIGLDToMint > 0) {
            IGLD_TOKEN.mint(msg.sender, totalIGLDToMint);
        }

        _createPosition(msg.sender, legs, PositionType.Spread, 0, 0);
    }

    // =============================================================
    //                     CLOSING LOGIC
    // =============================================================

    /**
     * @notice Automates closing based on Oracle price.
     * @dev Only callable by Keeper. Checks SL/TP and permission.
     */
    function triggerCloseByKeeper(address user, uint256 posId, address payoutToken) external nonReentrant whenNotPaused {
        require(isKeeper[msg.sender], "IGLDOTC: Caller is not a keeper");
        
        Position storage pos = userPositions[user][posId];
        require(pos.isActive, "IGLDOTC: Position not active");
        
        // 1. Verify Price Trigger (Oracle Check)
        if (pos.positionType == PositionType.Outright && pos.legs.length > 0) {
            // For SL/TP we usually check the current spot or the current futures price for that maturity
            // Here using futures price for accuracy of the specific contract
            uint256 currentPrice = priceFor(pos.legs[0].maturity);
            
            bool slHit = (pos.stopLossPrice > 0 && currentPrice <= pos.stopLossPrice);
            bool tpHit = (pos.takeProfitPrice > 0 && currentPrice >= pos.takeProfitPrice);
            
            require(slHit || tpHit, "IGLDOTC: SL or TP not hit yet");
        } 
        
        // 2. Execute Close
        _closePosition(user, posId, payoutToken, "Keeper Trigger");
    }

    function closePosition(uint256 positionId, address payoutToken) external payable nonReentrant whenNotPaused {
        _closePosition(msg.sender, positionId, payoutToken, "User Manual Close");
    }

    function _closePosition(address user, uint256 posId, address payoutToken, string memory reason) private {
        Position storage pos = userPositions[user][posId];
        require(pos.owner == user && pos.isActive, "IGLDOTC: Position invalid");

        int256 pnlValue = 0;
        uint256 totalBurnAmount = 0;

        for (uint i = 0; i < pos.legs.length; i++) {
            Leg storage leg = pos.legs[i];
            uint256 currentPrice = priceFor(leg.maturity);
            
            // PnL = (Current - Entry) * Amount
            int256 pnl = (int256(currentPrice) - int256(leg.entryPrice)) * int256(leg.amountIGLD) / 1e18;

            if (leg.isLong) {
                pnlValue += pnl;
                totalBurnAmount += leg.amountIGLD;
            } else {
                pnlValue -= pnl;
            }
        }

        pos.isActive = false;
        pos.closeTimestamp = block.timestamp;

        // 3. Permissioned Token Retrieval
        // Because we enforced Approval in openPosition, safeTransferFrom should succeed here
        // even if the user is not the one calling this function (Keeper case).
        if (totalBurnAmount > 0) {
            IGLD_TOKEN.safeTransferFrom(user, address(this), totalBurnAmount);
            IGLD_TOKEN.burn(totalBurnAmount);
        }

        // 4. Settlement
        uint256 fee = (pnlValue > 0 ? uint256(pnlValue) * treasuryFeeBPS : 0) / 10000;
        int256 netSettlement = pnlValue - int256(fee);

        if (netSettlement > 0) { // User Profit
            uint256 profit = uint256(netSettlement);
            if (payoutToken == address(0)) {
                // Warning: Contract must be funded with ETH to pay profit in ETH
                (bool sent, ) = payable(user).call{value: profit}("");
                require(sent, "IGLDOTC: Failed to send ETH profit");
            } else {
                // Contract must have IUSD balance (Treasury/Pool)
                IUSD_TOKEN.safeTransfer(user, profit);
            }
        } else if (netSettlement < 0) { // User Loss
            uint256 loss = uint256(-netSettlement);
            
            // If Keeper triggers this, the user can't pay ETH via msg.value.
            // Losses must be settled by pulling IUSD from user or assumed collateral (not implemented here).
            // For this logic, we assume settlement via IUSD pull (requires IUSD allowance).
            // If IUSD allowance missing, Keeper call will fail (safety mechanism).
            if (payoutToken == address(0)) {
                // If user closes manually, they can send ETH value
                 require(msg.value >= loss, "IGLDOTC: Insufficient ETH sent for loss");
                 (bool sent, ) = payable(treasuryAddress).call{value: loss}("");
                 require(sent, "IGLDOTC: ETH loss transfer failed");
            } else {
                IUSD_TOKEN.safeTransferFrom(user, treasuryAddress, loss);
            }
        }

        // Send Fee to Treasury
        if (fee > 0 && pnlValue > 0) {
             if (payoutToken == address(0)) {
                 // Fee is taken from contract balance if profit was paid out
                 (bool sent, ) = payable(treasuryAddress).call{value: fee}("");
                 require(sent, "IGLDOTC: Fee ETH transfer failed");
             } else {
                 IUSD_TOKEN.safeTransfer(treasuryAddress, fee);
             }
        }

        emit PositionClosed(user, posId, pnlValue, payoutToken, reason);
    }

    // =============================================================
    //                       HELPERS
    // =============================================================

    function _executeTrade(int256 netCost, address paymentToken, uint256 maxCost, uint256 minCredit) internal {
        if (netCost == 0) return;

        uint256 fee = (netCost > 0 ? uint256(netCost) * treasuryFeeBPS : uint256(-netCost) * treasuryFeeBPS) / 10000;

        if (netCost > 0) { // Debit (User Pays)
            uint256 totalCost = uint256(netCost) + fee;
            require(totalCost <= maxCost, "IGLDOTC: Slippage: Cost exceeds maxCost");

            if (paymentToken == address(0)) {
                require(msg.value >= totalCost, "IGLDOTC: Insufficient ETH sent");
                (bool sent, ) = payable(treasuryAddress).call{value: totalCost}("");
                require(sent, "IGLDOTC: ETH transfer failed");
            } else {
                IUSD_TOKEN.safeTransferFrom(msg.sender, treasuryAddress, totalCost);
            }
        } else { // Credit (User Receives)
            uint256 totalCredit = uint256(-netCost);
            require(totalCredit >= minCredit, "IGLDOTC: Slippage: Credit less than minCredit");
            
            uint256 creditAfterFee = totalCredit > fee ? totalCredit - fee : 0; 

            if (paymentToken == address(0)) {
                (bool creditSent, ) = payable(msg.sender).call{value: creditAfterFee}("");
                require(creditSent, "IGLDOTC: ETH credit transfer failed");
                (bool feeSent, ) = payable(treasuryAddress).call{value: fee}("");
                require(feeSent, "IGLDOTC: Fee transfer failed");
            } else {
                IUSD_TOKEN.safeTransfer(treasuryAddress, fee);
                IUSD_TOKEN.safeTransfer(msg.sender, creditAfterFee);
            }
        }
    }

    function _createPosition(address u, Leg[] memory legs, PositionType pt, uint256 sl, uint256 tp) private {
        uint256 positionId = nextPositionIdForUser[u];
        Position storage newPos = userPositions[u][positionId];
        newPos.positionId = positionId;
        newPos.owner = u;
        newPos.purchaseTimestamp = block.timestamp;
        newPos.isActive = true;
        newPos.positionType = pt;
        newPos.stopLossPrice = sl;
        newPos.takeProfitPrice = tp;
        for (uint i = 0; i < legs.length; i++) {
            newPos.legs.push(legs[i]);
        }
        nextPositionIdForUser[u]++;
        emit PositionOpened(u, positionId, pt, legs, sl, tp);
    }

    function _validateMaturity(uint256 maturity) private view {
        require(maturity > block.timestamp + minMaturityOffset, "IGLDOTC: Maturity too soon");
        require(maturity < block.timestamp + maxMaturityOffset, "IGLDOTC: Maturity too far");
    }

    // =============================================================
    //                      ADMIN & VIEWS
    // =============================================================

    function setOracleMaxAge(uint256 v) external onlyAdmin {
        oracleMaxAge = v;
        emit AdminSettingsChanged(msg.sender, "oracleMaxAge", v);
    }
    function setPriceSource(PriceSource s) external onlyAdmin {
        spotSource = s;
        emit AdminSettingsChanged(msg.sender, "spotSource", uint256(uint8(s)));
    }
    function setManualPrice(uint256 p) external onlyAdmin {
        manualSpotPrice = p;
        emit AdminSettingsChanged(msg.sender, "manualSpotPrice", p);
    }
    function setMaturityOffsets(uint256 min, uint256 max) external onlyAdmin {
        minMaturityOffset = min;
        maxMaturityOffset = max;
        emit AdminSettingsChanged(msg.sender, "minMaturityOffset", min);
        emit AdminSettingsChanged(msg.sender, "maxMaturityOffset", max);
    }
    function setTreasury(address t) external onlyAdmin {
        require(t != address(0), "IGLDOTC: Zero address");
        treasuryAddress = t;
        emit AdminAddressChanged(msg.sender, "treasuryAddress", t);
    }
    function setAnnualRateBPS(uint256 r) external onlyAdmin {
        annualRateBPS = r;
        emit AdminSettingsChanged(msg.sender, "annualRateBPS", r);
    }
    function setTreasuryFee(uint256 f) external onlyAdmin {
        require(f <= 1000, "IGLDOTC: Fee too high");
        treasuryFeeBPS = f;
        emit AdminSettingsChanged(msg.sender, "treasuryFeeBPS", f);
    }
    function setKeeper(address k, bool s) external onlyAdmin {
        isKeeper[k] = s;
        emit KeeperUpdated(k, s);
    }
    function rescueTokens(address t, address to, uint256 a) external onlyAdmin {
        require(t != address(IGLD_TOKEN) && t != address(IUSD_TOKEN), "IGLDOTC: Cannot rescue core tokens");
        IERC20(t).safeTransfer(to, a);
    }
    function pause() external onlyAdmin { _pause(); }
    function unpause() external onlyAdmin { _unpause(); }

    function getPositionById(address owner, uint256 positionId) external view returns (Position memory) {
        return userPositions[owner][positionId];
    }
    
    // To fund contract with ETH for paying out profits
    receive() external payable {}
}