contract Escrow is Ownable {
    event agreementInitialized(uint256 amount);
    event payment(uint256 amount);
    event AgreementDefaulted(uint256 outstandingObligations);

    struct TokenInfo {
        address EYE;
        address USDC; // 6 decimal places!
        uint256 initialUSDCDebt;
        uint256 initialEYEDeposit;
        uint256 monthlyPayment;
    }

    struct UserInfo {
        address Justin;
        address DGVC;
    }

    enum AgreementPhaseEnum {
        dormant,
        initialized,
        defaulted,
        concluded,
        emergencyShutdown
    }

    struct AgreementState {
        AgreementPhaseEnum phase;
        uint256 commencementTimeStamp;
        uint256 accumulatedRepayments;
    }

    TokenInfo tokens;
    UserInfo users;
    AgreementState agreementState;
    uint256 constant ONE_USDC = 1e6;
    uint256 constant ONE_EYE = 1e18;
    uint256 constant ARITHMETIC_FACTOR = 1e12;
    uint8 constant EMERGENCY_SHUTDOWN_JUSTIN_INDEX = 0;
    uint8 constant EMERGENCY_SHUTDOWN_DEGEN_INDEX = 1;
    bool[2] emergencyShutdownMultisig;
    bool public testing; //this must be false on mainnet

    constructor() {
        users.DGVC = 0x8b6e96947349C5eFAbD44Bd8f8901D31951202c6;
        users.Justin = msg.sender;

        tokens.EYE = 0x155ff1A85F440EE0A382eA949f24CE4E0b751c65;
        tokens.USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
        tokens.initialEYEDeposit = EYE(614521);
        tokens.initialUSDCDebt = USDC(550000);
        tokens.monthlyPayment = tokens.initialUSDCDebt / 18;

        agreementState.phase = AgreementPhaseEnum.dormant;
    }

    function setAddresses(
        address eye,
        address usdc,
        address dgvc
    ) public onlyOwner {
        uint256 id;
        assembly {
            id := chainid()
        }
        require(id != 1, "ESCROW: addresses hardcoded on mainnet.");
        tokens.EYE = eye;
        tokens.USDC = usdc;
        users.DGVC = dgvc;
    }

    function initializeAgreement(uint256 USDC_units) public {
        require(USDC_units < 550001, "ESCROW: Check your input");
        agreementState.phase = AgreementPhaseEnum.initialized;
        agreementState.commencementTimeStamp = block.timestamp;
        IERC20 eye = IERC20(tokens.EYE);
        uint256 balance = eye.balanceOf(address(this));
        require(
            balance >= tokens.initialEYEDeposit,
            "ESCROW: Degen must deposit EYE in order for agreement to commence."
        );
        emit agreementInitialized(USDC_units);
        payDebtFrom(USDC_units, msg.sender);
    }

    function payDebt(uint256 USDC_units) public {
        payDebtFrom(USDC_units, msg.sender);
    }

    function payDebtFrom(uint256 USDC_units, address payer) internal {
        require(
            agreementState.phase == AgreementPhaseEnum.initialized,
            "ESCROW: cannot pay on inactive escrow."
        );
        uint256 usdc = USDC(USDC_units);

        //payments made by debtor
        agreementState.accumulatedRepayments += usdc;

        //end debt binding if all obligations met
        if (agreementState.accumulatedRepayments >= tokens.initialUSDCDebt) {
            agreementState.phase = AgreementPhaseEnum.concluded;
            usdc -=
                agreementState.accumulatedRepayments -
                tokens.initialUSDCDebt;
        }

        require(
            IERC20(tokens.USDC).transferFrom(payer, users.DGVC, usdc),
            "ESCROW: debtor payment failed"
        );

        uint256 proportionalPayment =
            (usdc * ARITHMETIC_FACTOR) / tokens.initialUSDCDebt;
        uint256 eyeToWithdraw =
            (proportionalPayment * tokens.initialEYEDeposit) /
                ARITHMETIC_FACTOR;
        IERC20 eyeToken = IERC20(tokens.EYE);
        uint256 balance = eyeToken.balanceOf(address(this));
        if (
            agreementState.phase == AgreementPhaseEnum.concluded ||
            balance < eyeToWithdraw
        ) {
            eyeToWithdraw = balance;
        }

        require(
            eyeToken.transfer(users.Justin, eyeToWithdraw),
            "ERC20: token transfer failed"
        );

        emit payment(usdc);
    }

    function callEYE() public {
        require(
            msg.sender == users.DGVC,
            "ESCROW: only DGVC can call bad debt."
        );
        require(
            agreementState.phase == AgreementPhaseEnum.initialized,
            "ESCROW: calling bad debt can only be done when agreement is active."
        );

        //only claim if debtor is more than a month behind on repayments

        require(!isDebtorHealthy(), "ESCROW: debtor in healthy position.");
        IERC20 eye = IERC20(tokens.EYE);
        uint256 remainingEye = eye.balanceOf(address(this));
        eye.transfer(users.DGVC, remainingEye);
        agreementState.phase = AgreementPhaseEnum.defaulted;
        emit AgreementDefaulted(
            tokens.initialUSDCDebt - agreementState.accumulatedRepayments
        );
    }

    //if true, degen can't call their debt
    function isDebtorHealthy() public view returns (bool) {
        int256 months = monthsAhead();
        bool moreThanMonthBehind = months <= -1;
        if (months == 0) {
            //accounts for being at least 1 day overdue
            int256 arrears =
                int256(expectedPayments()) -
                    int256(agreementState.accumulatedRepayments);
            moreThanMonthBehind = arrears > 0;
        }
        return !moreThanMonthBehind;
    }

    function degenWithdraw() public {
        require(
            msg.sender == users.DGVC,
            "ESCROW: only DegenVC can call this function"
        );
        require(
            agreementState.phase == AgreementPhaseEnum.dormant,
            "ESCROW: Agreement has commenced."
        );
        uint256 balance = IERC20(tokens.EYE).balanceOf(address(this));
        IERC20(tokens.EYE).transfer(users.DGVC, balance);
    }

    function expectedPayments() public view returns (uint256) {
        uint256 monthsElapsed = getMonthsElapsed();
        return tokens.monthlyPayment * monthsElapsed;
    }

    //positive number means Justin has more than met his requirement. Negative means he's in arrears
    function monthsAhead() public view returns (int256 months) {
        uint256 expected = expectedPayments();
        int256 difference =
            int256(agreementState.accumulatedRepayments) - int256(expected);
        months = difference / int256(tokens.monthlyPayment);
    }

    function getMonthsElapsed() public view returns (uint256 monthsElapsed) {
        monthsElapsed = getDaysElapsed() / 31;

        if (monthsElapsed > 18) {
            monthsElapsed = 18;
        }
    }

    function getDaysElapsed() public view returns (uint256 daysElapsed) {
        daysElapsed =
            (block.timestamp - agreementState.commencementTimeStamp) /
            (1 days);
    }

    //positive number means Justin has more than met his requirement. Negative means he's in arrears
    function expectedAccumulated() public view returns (uint256, uint256) {
        uint256 expected = expectedPayments();

        if (expected > agreementState.accumulatedRepayments) {
            //Justin is behind

            return (expected, agreementState.accumulatedRepayments);
        } else {
            //Justin is ahead

            return (expected, agreementState.accumulatedRepayments);
        }
    }

    //in the event of a critical bug, shutdown contract and withdraw EYE.
    function voteForEmergencyShutdown(bool vote) public {
        if (msg.sender == users.Justin) {
            emergencyShutdownMultisig[EMERGENCY_SHUTDOWN_JUSTIN_INDEX] = vote;
        } else if (msg.sender == users.DGVC) {
            emergencyShutdownMultisig[EMERGENCY_SHUTDOWN_DEGEN_INDEX] = vote;
        }

        if (emergencyShutdownMultisig[0] && emergencyShutdownMultisig[1]) {
            agreementState.phase = AgreementPhaseEnum.emergencyShutdown;
            IERC20 eye = IERC20(tokens.EYE);
            uint256 balance = eye.balanceOf(address(this));
            eye.transfer(users.DGVC, balance);
        }
    }

    function getTokenInfo()
        external
        view
        returns (
            address,
            address,
            uint256,
            uint256,
            uint256
        )
    {
        return (
            tokens.EYE,
            tokens.USDC,
            tokens.initialUSDCDebt,
            tokens.initialEYEDeposit,
            tokens.monthlyPayment
        );
    }

    function getUserInfo() external view returns (address, address) {
        return (users.Justin, users.DGVC);
    }

    function getAgreementState()
        external
        view
        returns (
            uint256,
            uint256,
            uint256
        )
    {
        return (
            uint256(agreementState.phase),
            agreementState.commencementTimeStamp,
            agreementState.accumulatedRepayments
        );
    }

    function changeJustin(address newJustin) public {
        require(msg.sender == users.Justin);
        users.Justin = newJustin;
    }

    function changeDGVC(address newDGVC) public {
        require(msg.sender == users.DGVC);
        users.DGVC = newDGVC;
    }

    function USDC(uint256 units) public pure returns (uint256) {
        return units * ONE_USDC;
    }

    function EYE(uint256 units) public pure returns (uint256) {
        return units * ONE_EYE;
    }

    function MathMin(uint256 LHS, uint256 RHS) internal pure returns (uint256) {
        return LHS > RHS ? RHS : LHS;
    }
}
function voteForEmergencyShutdown(bool vote) public {
    if (msg.sender == users.Justin) {
        emergencyShutdownMultisig[EMERGENCY_SHUTDOWN_JUSTIN_INDEX] = vote;
    } else if (msg.sender == users.DGVC) {
        emergencyShutdownMultisig[EMERGENCY_SHUTDOWN_DEGEN_INDEX] = vote;
    }

    if (emergencyShutdownMultisig[0] && emergencyShutdownMultisig[1]) {
        agreementState.phase = AgreementPhaseEnum.emergencyShutdown;
        IERC20 eye = IERC20(tokens.EYE);
        uint256 balance = eye.balanceOf(address(this));
        eye.transfer(users.DGVC, balance);
    }
}

//in the event of a critical bug, shutdown contract and withdraw EYE.