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

import {IStaking} from "./interfaces/IStaking.sol";
import {IGatekeeper} from "./interfaces/IGatekeeper.sol";
import {IStorageHistory} from "./interfaces/IStorageHistory.sol";

import {StorageHistory} from "./types/StorageHistory.sol";
import {Weaver} from "./types/Weaver.sol";

import {FullMath} from "./libraries/FullMath.sol";
import {Checkpoints} from "./libraries/Checkpoints.sol";
import {RotationPacking, RequestPacking} from "./libraries/Packing.sol";
import {ReentrancyGuard} from "@openzeppelin-contracts/utils/ReentrancyGuard.sol";

contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard  {
    using Checkpoints for Checkpoints.Trace256;
    using RotationPacking for RotationPacking.RotationState;
    using RequestPacking for RequestPacking.RequestPayload;

    uint256 private constant COMMISSION_DIVISOR = type(uint32).max;
    uint256 private constant SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
    uint256 private constant SECP256K1_Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
    uint256 private constant SHIFT_FACTOR = (2**128) % SECP256K1_N;

    address public override staking;
    address public override storageHistory;
    uint256 public override existentialDeposit;

    address public deployer;
    address private _previousAddress;

    Checkpoints.Trace256 private _aggregatedPublicKeys;
    mapping(bytes32 => uint256) private _packedRotationStates;

    constructor(
        uint256 _existentialDeposit,
        address _previousGatekeeperAddress
    ) Weaver(_previousGatekeeperAddress) {
        existentialDeposit = _existentialDeposit;

        if (_previousGatekeeperAddress != address(0)) {
            require(_previousGatekeeperAddress != address(0));
            require(_previousGatekeeperAddress != address(this));
            _previousAddress = _previousGatekeeperAddress;

            storageHistory = IGatekeeper(_previousGatekeeperAddress).storageHistory();
            staking = IGatekeeper(_previousGatekeeperAddress).staking();
        } else {
            StorageHistory newStorageHistory = new StorageHistory(msg.sender);
            storageHistory = address(newStorageHistory);
            staking = msg.sender;
            deployer = tx.origin;
        }
    }

    function updatePublicKeyMetadata(uint256 exodusSession, bytes32 publicKey, uint8 parity) external {
        require(msg.sender == deployer);
        // TODO: uncomment line below, needed only for testing purposes
        // and should push to hardcoded exodusSession 0 always
        // deployer = address(0);

        // forge-lint: disable-next-line(unsafe-typecast)
        uint256 packedRotationState = RotationPacking.pack(parity, uint64(exodusSession));

        _aggregatedPublicKeys.push(exodusSession, uint256(publicKey));
        _packedRotationStates[publicKey] = packedRotationState;
    }

    function previousAddress() external override view returns (address) {
        return _previousAddress;
    }

    function ghostedSupply() external override view returns (uint256) {
        return IStorageHistory(storageHistory).bridgeImbalance();
    }

    function latestPublicKeyInfo() external override view returns (bytes32, uint8, uint64) {
        bytes32 latestPublicKey = bytes32(_aggregatedPublicKeys.latest());
        uint256 packed = _packedRotationStates[latestPublicKey];

        RotationPacking.RotationState memory state = RotationPacking.unpack(packed);
        return (latestPublicKey, state.parity, state.session);
    }

    function getRotationInfoAt(uint256 exodusSession) public override view returns (bytes32, uint8, uint64) {
        bytes32 publicKey = bytes32(_aggregatedPublicKeys.upperLookup(exodusSession));
        uint256 packed = _packedRotationStates[publicKey];
        RotationPacking.RotationState memory state = RotationPacking.unpack(packed);

        if (exodusSession < state.session) {
            return IGatekeeper(_previousAddress).getRotationInfoAt(exodusSession);
        }

        return (publicKey, state.parity, state.session);
    }

    function verify(
        bytes calldata call,
        bytes calldata nonce,
        bytes32 s
    ) external nonReentrant payable returns (bytes memory) {
        _verifySchnorrSignature(call, nonce, s);

        (bool success, bytes memory data) = address(this).call{ value: msg.value }(call);
        if (!success) revert ExecutionReverted();

        return data;
    }

    function ghost(bytes32 receiver, uint256 amount) external override returns (uint256) {
        if (msg.sender != staking) revert NotStaking();
        if (amount < existentialDeposit) revert NonExistentAmount();

        IStorageHistory(storageHistory).increaseBridgeIn(amount);

        emit Ghosted(receiver, amount);
        return _insertTreeNode(receiver, amount);
    }

    function materialize(
        uint256 exodusSession,
        uint256 amount,
        uint256 packed
    ) external override payable {
        if (msg.sender != address(this)) revert NotGatekeeper();

        RequestPacking.RequestPayload memory payload = RequestPacking.unpack(packed);
        if (payload.chainId != block.chainid) revert WrongChainId();

        StorageHistory(storageHistory).trySetTransactionExecuted(exodusSession);
        IStorageHistory(storageHistory).tryIncreaseBridgeOut(amount);

        uint256 commissionAmount = FullMath.mulDiv(amount, uint256(payload.commission), COMMISSION_DIVISOR);
        uint256 receiverAmount = amount - commissionAmount;

        IStaking(staking).materialize(payload.receiver, receiverAmount);

        if (commissionAmount > 0) {
            uint256 totalReserves = IStaking(staking).totalReserves();
            uint256 baseSupply = IStaking(staking).baseSupply();
            uint256 minimumNativeRequired = FullMath.mulDiv(commissionAmount, totalReserves, baseSupply);

            if (minimumNativeRequired > msg.value) revert InsufficientValue();
            IStaking(staking).materialize(tx.origin, commissionAmount);

            (bool sentSuccess,) = payload.receiver.call{ value: msg.value }("");
            if (!sentSuccess) revert SendFailed();
        }

        emit Materialized(payload.receiver, amount);
    }

    function rotate(
        uint256 exodusSession,
        bytes32 newPublicKey,
        uint8 newParity
    ) external override {
        if (msg.sender != address(this)) revert NotGatekeeper();
        StorageHistory(storageHistory).trySetTransactionExecuted(exodusSession);

        // forge-lint: disable-next-line(unsafe-typecast)
        uint256 packedRotationState = RotationPacking.pack(newParity, uint64(exodusSession));

        _aggregatedPublicKeys.push(exodusSession, uint256(newPublicKey));
        _packedRotationStates[newPublicKey] = packedRotationState;

        emit Rotated(newPublicKey, newParity);
    }

    function _verifySchnorrSignature(bytes calldata call, bytes calldata nonce, bytes32 s) internal view {
        (bytes32 px, uint8 p) = _extractPublicKey(call);

        uint8 v = p == 0 ? 27 : 28;
        bytes32 e = _challenge(call, nonce, px, p);

        bytes32 sp = bytes32(SECP256K1_Q - mulmod(uint256(s), uint256(px), SECP256K1_Q));
        bytes32 ep = bytes32(SECP256K1_Q - mulmod(uint256(e), uint256(px), SECP256K1_Q));
        address recoveredR = ecrecover(sp, v, px, ep);

        if (sp == 0) revert BadSignature();
        if (recoveredR == address(0)) revert BadSignature();
        if (recoveredR != _uncompressedPointToAddress(nonce)) revert BadSignature();
    }

    function _uncompressedPointToAddress(bytes calldata nonce) internal pure returns (address) {
        if (nonce.length != 65) revert BadSignature();
        if (nonce[0] != 0x04) revert BadSignature();

        bytes32 rx = bytes32(nonce[1:33]);
        bytes32 ry = bytes32(nonce[33:65]);

        return address(uint160(uint256(keccak256(abi.encodePacked(rx, ry)))));
    }

    function _extractPublicKey(bytes calldata call) internal view returns (bytes32, uint8) {
        if (call.length < 36) revert InvalidCalldata();
        uint256 exodusSession;
        assembly {
            exodusSession := calldataload(add(call.offset, 4))
        }

        (bytes32 publicKey, uint8 parity,) = getRotationInfoAt(exodusSession);

        return (publicKey, parity);
    }

    function _challenge(
        bytes calldata call,
        bytes calldata r,
        bytes32 pk,
        uint8 v
    ) internal pure returns (bytes32) {
        if (r.length != 65) revert BadSignature();

        bytes32 rx = bytes32(r[1:33]);
        bytes1 rv;

        {
            bytes32 ry = bytes32(r[33:65]);
            uint8 rParityByte = (uint256(ry) & 1) == 0 ? 0x02 : 0x03;
            rv = bytes1(rParityByte);
        }

        uint8 pv = v == 0 ? 0x02 : 0x03;

        bytes memory message = abi.encodePacked(rv, rx, pv, pk, call);
        bytes memory domain = "FROST-secp256k1-SHA256-v1chal";
        uint8 dstlen = uint8(domain.length);

        bytes32 b0 = sha256(abi.encodePacked(
            bytes32(0),
            bytes32(0),
            message,
            hex"0030",
            uint8(0),
            domain,
            dstlen
        ));

        bytes32 b1 = sha256(abi.encodePacked(b0, uint8(1), domain, dstlen));
        bytes32 b2 = sha256(abi.encodePacked(b0 ^ b1, uint8(2), domain, dstlen));

        uint256 high32 = uint256(b1);
        uint256 lowPart = uint256(b2) >> 128;

        uint256 highReduced = mulmod(high32, SHIFT_FACTOR, SECP256K1_N);
        uint256 finalChallenge = addmod(highReduced, lowPart, SECP256K1_N);

        return bytes32(finalChallenge);
    }
}
