// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

/// @title BasePilotAccessControllerCoreV3
/// @notice Shared immutable-chain pilot gate for factories and modules.
/// @dev This contract holds no assets and exposes no withdrawal path. Modules
///      opt in by registering positions before accepting pilot value and by
///      releasing value after redemptions. Releasing is intentionally allowed
///      even while new deploys and writes are disabled.
/// @dev This is an implementation detail. Deploy only one of the explicitly
/// named profiles below; callers must never select a chain at deployment time.
abstract contract BasePilotAccessControllerCoreV3 {
    bytes32 public constant SUPPORTED_PILOT_MODULE_ID = keccak256("Vaultfire.BaseV3.SafeBondFactory");
    bytes32 public constant GOVERNANCE_POLICY_MODULE_ID = keccak256("Vaultfire.BaseV3.PilotController");
    uint256 public immutable requiredChainId;
    /// @dev Matches the governance pause-target maximum, so every marked child
    ///      can be independently covered before writes are enabled.
    uint256 public constant MAX_GUARDIAN_COVERED_POSITIONS = 32;

    address public immutable governance;
    /// @notice Fresh-deployment liability ceiling for all registered positions.
    /// @dev This is intentionally immutable so governance may lower and later
    ///      restore configured caps, but can never raise pilot exposure above
    ///      the reviewed deployment-time maximum.
    uint256 public immutable maxGlobalCap;
    /// @notice Fresh-deployment liability ceiling for each registered position.
    uint256 public immutable maxPerPositionCap;
    bool public deployEnabled;
    bool public writeEnabled;
    uint256 public globalCap;
    uint256 public perPositionCap;
    uint256 public totalAllocated;
    uint256 public designPartnerCount;
    uint256 public moduleCount;

    mapping(address => bool) public isDesignPartner;
    mapping(address => bool) public isModule;
    mapping(address => address) public positionDesignPartner;
    mapping(address => uint256) public positionAllocated;
    mapping(address => uint256) public positionCap;
    mapping(address => bool) public guardianPauseCoverageRequired;
    address[] private _guardianCoveredPositions;

    error NotGovernance();
    error NotModule();
    error UnsupportedChain();
    error ZeroAddress();
    error InvalidCap();
    error InvalidMaximumCap();
    error DeployDisabled();
    error WriteDisabled();
    error DesignPartnerNotAllowed();
    error PositionAlreadyRegistered();
    error PositionNotRegistered();
    error PositionCapExceeded();
    error GlobalCapExceeded();
    error AmountZero();
    error AmountExceedsPositionAllocation();
    error ModuleMustBeContract();
    error DesignPartnerLimitReached();
    /// @notice Controller writes cannot be enabled unless governance has
    ///         registered the position as a guardian pause target.
    error GuardianPauseCoverageMissing(address position);
    error PositionMustBeContract();
    error GuardianCoveredPositionLimitReached();
    error UnsupportedModule();

    event DesignPartnerSetV3(address indexed partner, bool allowed);
    event ModuleSetV3(address indexed module, bool allowed);
    event CapsSetV3(uint256 globalCap, uint256 perPositionCap);
    event DeployEnabledSetV3(bool enabled);
    event WriteEnabledSetV3(bool enabled);
    event PositionRegisteredV3(
        address indexed position, address indexed designPartner, uint256 initialAllocation, uint256 positionCap
    );
    event PositionAllocationIncreasedV3(address indexed position, uint256 amount, uint256 newAllocation);
    event PositionAllocationReleasedV3(address indexed position, uint256 amount, uint256 newAllocation);

    modifier onlyGovernance() {
        if (msg.sender != governance) revert NotGovernance();
        _;
    }

    modifier onlyModule() {
        if (!isModule[msg.sender]) revert NotModule();
        _;
    }

    /// @dev A registered position may update only its own allocation. Approved
    ///      modules retain the ability to operate positions they administer.
    ///      This lets a factory register a freshly deployed position without
    ///      granting that position blanket module authority.
    modifier onlyModuleOrPosition(address position) {
        if (msg.sender != position && !isModule[msg.sender]) revert NotModule();
        _;
    }

    constructor(uint256 _requiredChainId, address _governance, uint256 _maxGlobalCap, uint256 _maxPerPositionCap) {
        if (block.chainid != _requiredChainId) revert UnsupportedChain();
        if (_governance == address(0)) revert ZeroAddress();
        if (_maxGlobalCap == 0 || _maxPerPositionCap == 0 || _maxPerPositionCap > _maxGlobalCap) {
            revert InvalidMaximumCap();
        }
        requiredChainId = _requiredChainId;
        governance = _governance;
        maxGlobalCap = _maxGlobalCap;
        maxPerPositionCap = _maxPerPositionCap;
    }

    // ---------------------------------------------------------------------
    // Governance configuration
    // ---------------------------------------------------------------------

    function setDesignPartner(address partner, bool allowed) external onlyGovernance {
        if (partner == address(0)) revert ZeroAddress();
        bool currentlyAllowed = isDesignPartner[partner];
        if (currentlyAllowed == allowed) {
            emit DesignPartnerSetV3(partner, allowed);
            return;
        }
        if (allowed) {
            if (designPartnerCount != 0) revert DesignPartnerLimitReached();
            designPartnerCount = 1;
        } else {
            designPartnerCount = 0;
        }
        isDesignPartner[partner] = allowed;
        emit DesignPartnerSetV3(partner, allowed);
    }

    function setModule(address module, bool allowed) external onlyGovernance {
        if (module == address(0)) revert ZeroAddress();
        if (allowed) {
            if (module.code.length == 0) revert ModuleMustBeContract();
            (bool idOk, bytes memory idData) = module.staticcall(abi.encodeWithSignature("supportedPilotModuleId()"));
            (bool controllerOk, bytes memory controllerData) =
                module.staticcall(abi.encodeWithSignature("pilotController()"));
            if (
                !idOk || idData.length != 32 || abi.decode(idData, (bytes32)) != SUPPORTED_PILOT_MODULE_ID
                    || !controllerOk || controllerData.length != 32
                    || abi.decode(controllerData, (address)) != address(this)
            ) revert UnsupportedModule();
        }
        if (isModule[module] != allowed) {
            if (allowed) {
                ++moduleCount;
            } else {
                --moduleCount;
            }
        }
        isModule[module] = allowed;
        emit ModuleSetV3(module, allowed);
    }

    /// @notice Both caps must be configured before a module can register a
    ///         position. The global cap cannot be set below current allocation
    ///         or above the immutable fresh-deployment liability ceilings.
    function setCaps(uint256 newGlobalCap, uint256 newPerPositionCap) external onlyGovernance {
        if (
            newGlobalCap == 0 || newPerPositionCap == 0 || newGlobalCap < totalAllocated || newGlobalCap > maxGlobalCap
                || newPerPositionCap > maxPerPositionCap || newPerPositionCap > newGlobalCap
        ) revert InvalidCap();
        globalCap = newGlobalCap;
        perPositionCap = newPerPositionCap;
        emit CapsSetV3(newGlobalCap, newPerPositionCap);
    }

    function setDeployEnabled(bool enabled) external onlyGovernance {
        deployEnabled = enabled;
        emit DeployEnabledSetV3(enabled);
    }

    function setWriteEnabled(bool enabled) external onlyGovernance {
        if (enabled) _requireGuardianPauseCoverage();
        writeEnabled = enabled;
        emit WriteEnabledSetV3(enabled);
    }

    // ---------------------------------------------------------------------
    // Factory / module hooks
    // ---------------------------------------------------------------------

    /// @notice Called by an approved factory or module after it has created a
    ///         position and before it accepts the initial pilot allocation.
    function registerPosition(address designPartner, address position, uint256 initialAllocation) external onlyModule {
        _registerPosition(designPartner, position, initialAllocation);
    }

    /// @notice Registers a factory-created, value-bearing child position.
    /// @dev Unlike generic module positions, each child is subject to the
    ///      guardian-pause proof checked before writes can be enabled.
    function registerGuardianPauseablePosition(address designPartner, address position, uint256 initialAllocation)
        external
        onlyModule
    {
        if (position.code.length == 0) revert PositionMustBeContract();
        _registerPosition(designPartner, position, initialAllocation);
        if (_guardianCoveredPositions.length >= MAX_GUARDIAN_COVERED_POSITIONS) {
            revert GuardianCoveredPositionLimitReached();
        }
        guardianPauseCoverageRequired[position] = true;
        _guardianCoveredPositions.push(position);
    }

    function _registerPosition(address designPartner, address position, uint256 initialAllocation) private {
        if (!deployEnabled) revert DeployDisabled();
        if (!isDesignPartner[designPartner]) revert DesignPartnerNotAllowed();
        if (position == address(0)) revert ZeroAddress();
        if (positionDesignPartner[position] != address(0)) revert PositionAlreadyRegistered();

        _checkAllocation(initialAllocation);
        positionDesignPartner[position] = designPartner;
        positionAllocated[position] = initialAllocation;
        positionCap[position] = perPositionCap;
        totalAllocated += initialAllocation;
        emit PositionRegisteredV3(position, designPartner, initialAllocation, perPositionCap);
    }

    /// @notice Called by an approved module before accepting additional value
    ///         into a registered position.
    function increasePositionAllocation(address position, uint256 amount) external onlyModuleOrPosition(position) {
        if (!writeEnabled) revert WriteDisabled();
        if (amount == 0) revert AmountZero();

        address designPartner = positionDesignPartner[position];
        if (designPartner == address(0)) revert PositionNotRegistered();
        if (!isDesignPartner[designPartner]) revert DesignPartnerNotAllowed();
        if (guardianPauseCoverageRequired[position] && !hasGuardianPauseCoverage(position)) {
            revert GuardianPauseCoverageMissing(position);
        }

        uint256 newAllocation = positionAllocated[position] + amount;
        if (newAllocation > positionCap[position]) revert PositionCapExceeded();
        if (totalAllocated + amount > globalCap) revert GlobalCapExceeded();

        positionAllocated[position] = newAllocation;
        totalAllocated += amount;
        emit PositionAllocationIncreasedV3(position, amount, newAllocation);
    }

    /// @notice Called by an approved module after a withdrawal, redemption, or
    ///         settlement. This remains callable while the pilot is paused so
    ///         the controller never impedes withdrawals.
    function releasePositionAllocation(address position, uint256 amount) external onlyModuleOrPosition(position) {
        if (amount == 0) revert AmountZero();
        if (positionDesignPartner[position] == address(0)) revert PositionNotRegistered();

        uint256 currentAllocation = positionAllocated[position];
        if (amount > currentAllocation) revert AmountExceedsPositionAllocation();
        uint256 newAllocation = currentAllocation - amount;
        positionAllocated[position] = newAllocation;
        totalAllocated -= amount;
        emit PositionAllocationReleasedV3(position, amount, newAllocation);
    }

    function _checkAllocation(uint256 allocation) private view {
        if (allocation > perPositionCap) revert PositionCapExceeded();
        if (totalAllocated + allocation > globalCap) revert GlobalCapExceeded();
    }

    /// @notice Reports whether governance objectively exposes guardian pause
    ///         coverage for a registered position.
    /// @dev The controller deliberately reads the governance-owned,
    ///      append-only pause-target registry rather than keeping a procedural
    ///      local flag. An EOA, an incompatible governance contract, or a
    ///      malformed response is fail-closed.
    function hasGuardianPauseCoverage(address position) public view returns (bool) {
        (bool ok, bytes memory result) =
            governance.staticcall(abi.encodeWithSignature("isGuardianPauseTarget(address)", position));
        if (!ok || result.length < 32) return false;

        uint256 covered;
        assembly ("memory-safe") {
            covered := mload(add(result, 32))
        }
        return covered == 1;
    }

    function _requireGuardianPauseCoverage() private view {
        for (uint256 i; i < _guardianCoveredPositions.length; ++i) {
            address position = _guardianCoveredPositions[i];
            if (!hasGuardianPauseCoverage(position)) revert GuardianPauseCoverageMissing(position);
        }
    }
}

/// @title BasePilotAccessControllerV3
/// @notice Base mainnet (8453) pilot controller profile.
/// @dev This profile remains mainnet-only. It cannot be used for Base Sepolia.
contract BasePilotAccessControllerV3 is BasePilotAccessControllerCoreV3 {
    uint256 public constant BASE_CHAIN_ID = 8453;

    constructor(address _governance, uint256 _maxGlobalCap, uint256 _maxPerPositionCap)
        BasePilotAccessControllerCoreV3(BASE_CHAIN_ID, _governance, _maxGlobalCap, _maxPerPositionCap)
    { }
}
