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

import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.sol";
import "../registry/UniversalAssetRegistry.sol";

/**
 * @title GovernanceController
 * @notice Hybrid governance with progressive timelock based on asset risk
 * @dev Modes: Admin-only, 1-day timelock, 3-day + voting, 7-day + quorum
 */
contract GovernanceController is 
    Initializable,
    AccessControlUpgradeable,
    ReentrancyGuardUpgradeable,
    UUPSUpgradeable
{
    bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
    bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
    bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    bytes4 private constant SET_PRIMARY_JURISDICTION_SELECTOR = 0x997f4edf;

    // Governance modes
    enum GovernanceMode {
        AdminOnly,          // Mode 1: Admin can execute immediately
        TimelockShort,      // Mode 2: 1 day timelock
        TimelockModerate,   // Mode 3: 3 days + voting required
        TimelockLong        // Mode 4: 7 days + quorum required
    }

    // Proposal states
    enum ProposalState {
        Pending,
        Active,
        Canceled,
        Defeated,
        Succeeded,
        Queued,
        Expired,
        Executed
    }

    struct Proposal {
        uint256 proposalId;
        address proposer;
        address[] targets;
        uint256[] values;
        bytes[] calldatas;
        string description;
        uint256 startBlock;
        uint256 endBlock;
        uint256 eta;
        GovernanceMode mode;
        ProposalState state;
        uint256 forVotes;
        uint256 againstVotes;
        uint256 abstainVotes;
        mapping(address => bool) hasVoted;
    }

    // Storage
    UniversalAssetRegistry public assetRegistry;
    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => address) public proposalAssets;
    uint256 public proposalCount;
    
    // Governance parameters
    uint256 public votingDelay;     // Blocks to wait before voting starts
    uint256 public votingPeriod;    // Blocks voting is open
    uint256 public quorumNumerator; // Percentage required for quorum
    mapping(uint256 => bytes32) public proposalJurisdictionIds;
    mapping(uint256 => bytes32) public proposalTransitionJurisdictionIds;
    mapping(uint256 => bool) public proposalJurisdictionReviewRequired;
    mapping(uint256 => uint256) public proposalMinimumNoticePeriod;
    mapping(uint256 => uint256) public proposalJurisdictionApprovalCount;
    mapping(uint256 => uint256) public proposalTransitionJurisdictionApprovalCount;
    mapping(uint256 => address) public proposalLastJurisdictionApprover;
    mapping(uint256 => address) public proposalLastTransitionJurisdictionApprover;
    mapping(uint256 => mapping(address => bool)) private _proposalJurisdictionApprovals;
    mapping(uint256 => mapping(address => bool)) private _proposalTransitionJurisdictionApprovals;
    
    uint256 public constant TIMELOCK_SHORT = 1 days;
    uint256 public constant TIMELOCK_MODERATE = 3 days;
    uint256 public constant TIMELOCK_LONG = 7 days;
    uint256 public constant GRACE_PERIOD = 14 days;

    // Events
    event ProposalCreated(
        uint256 indexed proposalId,
        address proposer,
        address[] targets,
        uint256[] values,
        string[] signatures,
        bytes[] calldatas,
        uint256 startBlock,
        uint256 endBlock,
        string description
    );
    
    event VoteCast(
        address indexed voter,
        uint256 proposalId,
        uint8 support,
        uint256 weight,
        string reason
    );
    
    event ProposalQueued(uint256 indexed proposalId, uint256 eta);
    event ProposalExecuted(uint256 indexed proposalId);
    event ProposalCanceled(uint256 indexed proposalId);
    event ProposalJurisdictionApproved(
        uint256 indexed proposalId,
        bytes32 indexed jurisdictionId,
        address indexed authority,
        uint256 approvalCount
    );
    event ProposalAssetScoped(
        uint256 indexed proposalId,
        address indexed asset,
        bytes32 indexed jurisdictionId,
        bool reviewRequired,
        uint256 minimumNoticePeriod
    );

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(
        address _assetRegistry,
        address admin
    ) external initializer {
        __AccessControl_init();
        __ReentrancyGuard_init();
        __UUPSUpgradeable_init();
        
        require(_assetRegistry != address(0), "Zero registry");
        
        assetRegistry = UniversalAssetRegistry(_assetRegistry);
        
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(PROPOSER_ROLE, admin);
        _grantRole(EXECUTOR_ROLE, admin);
        _grantRole(CANCELLER_ROLE, admin);
        _grantRole(UPGRADER_ROLE, admin);
        votingDelay = 1; // 1 block
        votingPeriod = 50400; // ~7 days
        quorumNumerator = 4; // 4% quorum
    }

    function _authorizeUpgrade(address newImplementation)
        internal override onlyRole(UPGRADER_ROLE) {}

    /**
     * @notice Create a new proposal
     */
    function propose(
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        string memory description,
        GovernanceMode mode
    ) external pure returns (uint256) {
        targets;
        values;
        calldatas;
        description;
        mode;
        revert("Use proposeForAsset");
    }

    function proposeForAsset(
        address asset,
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        string memory description,
        GovernanceMode mode
    ) external onlyRole(PROPOSER_ROLE) returns (uint256) {
        require(targets.length == values.length, "Length mismatch");
        require(targets.length == calldatas.length, "Length mismatch");
        require(targets.length > 0, "Empty proposal");
        require(assetRegistry.isAssetActive(asset), "Asset inactive");

        UniversalAssetRegistry.UniversalAsset memory assetRecord = assetRegistry.getAsset(asset);
        bytes32 jurisdictionId = assetRegistry.getAssetJurisdictionId(asset);
        require(jurisdictionId != bytes32(0), "Asset jurisdiction missing");
        bytes32 transitionJurisdictionId = _validateProposalScope(asset, jurisdictionId, targets, calldatas);
        bool reviewRequired =
            assetRecord.governmentApprovalRequired ||
            assetRecord.supervisionRequired ||
            assetRecord.requiresGovernance;
        uint256 minimumNoticePeriod = assetRecord.minimumUpgradeNoticePeriod;
        uint256 jurisdictionNoticePeriod = assetRegistry.getJurisdictionMinimumUpgradeNotice(jurisdictionId);
        if (jurisdictionNoticePeriod > minimumNoticePeriod) {
            minimumNoticePeriod = jurisdictionNoticePeriod;
        }
        if (transitionJurisdictionId != bytes32(0) && transitionJurisdictionId != jurisdictionId) {
            reviewRequired = true;
            uint256 transitionNoticePeriod =
                assetRegistry.getJurisdictionMinimumUpgradeNotice(transitionJurisdictionId);
            if (transitionNoticePeriod > minimumNoticePeriod) {
                minimumNoticePeriod = transitionNoticePeriod;
            }
        }
        
        proposalCount++;
        uint256 proposalId = proposalCount;
        
        Proposal storage proposal = proposals[proposalId];
        proposal.proposalId = proposalId;
        proposal.proposer = msg.sender;
        proposal.targets = targets;
        proposal.values = values;
        proposal.calldatas = calldatas;
        proposal.description = description;
        proposal.startBlock = block.number + votingDelay;
        proposal.endBlock = proposal.startBlock + votingPeriod;
        proposal.mode = mode;
        proposal.state = ProposalState.Pending;
        proposalAssets[proposalId] = asset;
        proposalJurisdictionIds[proposalId] = jurisdictionId;
        proposalTransitionJurisdictionIds[proposalId] = transitionJurisdictionId;
        proposalJurisdictionReviewRequired[proposalId] = reviewRequired;
        proposalMinimumNoticePeriod[proposalId] = minimumNoticePeriod;
        
        emit ProposalCreated(
            proposalId,
            msg.sender,
            targets,
            values,
            new string[](targets.length),
            calldatas,
            proposal.startBlock,
            proposal.endBlock,
            description
        );
        emit ProposalAssetScoped(
            proposalId,
            asset,
            jurisdictionId,
            reviewRequired,
            minimumNoticePeriod
        );
        
        return proposalId;
    }

    /**
     * @notice Cast a vote on a proposal
     */
    function castVote(
        uint256 proposalId,
        uint8 support
    ) external returns (uint256) {
        return _castVote(msg.sender, proposalId, support, "");
    }

    /**
     * @notice Cast a vote with reason
     */
    function castVoteWithReason(
        uint256 proposalId,
        uint8 support,
        string calldata reason
    ) external returns (uint256) {
        return _castVote(msg.sender, proposalId, support, reason);
    }

    /**
     * @notice Internal vote casting
     */
    function _castVote(
        address voter,
        uint256 proposalId,
        uint8 support,
        string memory reason
    ) internal returns (uint256) {
        Proposal storage proposal = proposals[proposalId];
        
        require(state(proposalId) == ProposalState.Active, "Not active");
        require(!proposal.hasVoted[voter], "Already voted");
        require(support <= 2, "Invalid support");
        
        uint256 weight = _getVotes(voter);
        
        proposal.hasVoted[voter] = true;
        
        if (support == 0) {
            proposal.againstVotes += weight;
        } else if (support == 1) {
            proposal.forVotes += weight;
        } else {
            proposal.abstainVotes += weight;
        }
        
        emit VoteCast(voter, proposalId, support, weight, reason);
        
        return weight;
    }

    function _validateProposalScope(
        address asset,
        bytes32 currentJurisdictionId,
        address[] memory targets,
        bytes[] memory calldatas
    ) internal view returns (bytes32 transitionJurisdictionId) {
        for (uint256 i = 0; i < targets.length; i++) {
            address target = targets[i];
            bytes4 selector = _selector(calldatas[i]);
            if (target == asset) {
                if (selector == SET_PRIMARY_JURISDICTION_SELECTOR) {
                    bytes32 derivedJurisdictionId = assetRegistry.jurisdictionIdFor(
                        _decodePrimaryJurisdiction(calldatas[i])
                    );
                    if (transitionJurisdictionId != bytes32(0)) {
                        require(
                            transitionJurisdictionId == derivedJurisdictionId,
                            "Conflicting jurisdiction transition"
                        );
                    }
                    transitionJurisdictionId = derivedJurisdictionId;
                }
                continue;
            }

            require(target == address(assetRegistry), "Unsupported asset-scoped target");
            _requireRegistryCallScopedToAsset(asset, currentJurisdictionId, selector, calldatas[i]);
        }
    }

    function _requireRegistryCallScopedToAsset(
        address asset,
        bytes32,
        bytes4 selector,
        bytes memory data
    ) internal pure {
        if (
            selector == UniversalAssetRegistry.setJurisdictionProfile.selector ||
            selector == UniversalAssetRegistry.setJurisdictionAuthority.selector
        ) {
            revert("Use asset-derived jurisdiction registry entry point");
        }

        require(data.length >= 36, "Registry call not asset-scoped");
        address scopedAsset = _readAddressArgument(data);
        require(scopedAsset == asset, "Registry asset mismatch");
    }

    function _readAddressArgument(bytes memory data) internal pure returns (address scopedAsset) {
        assembly {
            scopedAsset := and(
                mload(add(data, 36)),
                0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff
            )
        }
    }

    function _selector(bytes memory data) internal pure returns (bytes4 selector) {
        require(data.length >= 4, "Invalid call data");
        assembly {
            selector := mload(add(data, 32))
        }
    }

    function _decodePrimaryJurisdiction(bytes memory data) internal pure returns (string memory jurisdiction) {
        jurisdiction = abi.decode(_stripSelector(data), (string));
        require(bytes(jurisdiction).length > 0, "Empty jurisdiction");
    }

    function _stripSelector(bytes memory data) internal pure returns (bytes memory payload) {
        require(data.length >= 4, "Invalid call data");
        payload = new bytes(data.length - 4);
        for (uint256 i = 4; i < data.length; i++) {
            payload[i - 4] = data[i];
        }
    }

    /**
     * @notice Queue a successful proposal
     */
    function queue(uint256 proposalId) external {
        require(state(proposalId) == ProposalState.Succeeded, "Not succeeded");
        
        Proposal storage proposal = proposals[proposalId];
        _requireJurisdictionApprovalIfNeeded(proposalId);
        uint256 delay = _getTimelockDelay(proposal.mode);
        if (proposalMinimumNoticePeriod[proposalId] > delay) {
            delay = proposalMinimumNoticePeriod[proposalId];
        }
        uint256 eta = block.timestamp + delay;
        
        proposal.eta = eta;
        proposal.state = ProposalState.Queued;
        
        emit ProposalQueued(proposalId, eta);
    }

    /**
     * @notice Execute a queued proposal
     */
    function execute(uint256 proposalId) external payable nonReentrant {
        require(state(proposalId) == ProposalState.Queued, "Not queued");
        
        Proposal storage proposal = proposals[proposalId];
        require(block.timestamp >= proposal.eta, "Timelock not met");
        require(block.timestamp <= proposal.eta + GRACE_PERIOD, "Expired");
        
        proposal.state = ProposalState.Executed;
        
        for (uint256 i = 0; i < proposal.targets.length; i++) {
            _executeTransaction(
                proposal.targets[i],
                proposal.values[i],
                proposal.calldatas[i]
            );
        }
        
        emit ProposalExecuted(proposalId);
    }

    /**
     * @notice Cancel a proposal
     */
    function cancel(uint256 proposalId) external onlyRole(CANCELLER_ROLE) {
        ProposalState currentState = state(proposalId);
        require(
            currentState != ProposalState.Executed &&
            currentState != ProposalState.Canceled,
            "Cannot cancel"
        );
        
        Proposal storage proposal = proposals[proposalId];
        proposal.state = ProposalState.Canceled;
        
        emit ProposalCanceled(proposalId);
    }

    /**
     * @notice Get proposal state
     */
    function state(uint256 proposalId) public view returns (ProposalState) {
        Proposal storage proposal = proposals[proposalId];
        
        if (proposal.state == ProposalState.Executed) return ProposalState.Executed;
        if (proposal.state == ProposalState.Canceled) return ProposalState.Canceled;
        if (proposal.state == ProposalState.Queued) {
            if (block.timestamp >= proposal.eta + GRACE_PERIOD) {
                return ProposalState.Expired;
            }
            return ProposalState.Queued;
        }
        
        if (block.number <= proposal.startBlock) return ProposalState.Pending;
        if (block.number <= proposal.endBlock) return ProposalState.Active;
        
        if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) {
            return ProposalState.Succeeded;
        } else {
            return ProposalState.Defeated;
        }
    }

    /**
     * @notice Execute transaction
     */
    function _executeTransaction(
        address target,
        uint256 value,
        bytes memory data
    ) internal {
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        
        if (!success) {
            if (returndata.length > 0) {
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert("Execution failed");
            }
        }
    }

    /**
     * @notice Get timelock delay based on governance mode
     */
    function _getTimelockDelay(GovernanceMode mode) internal pure returns (uint256) {
        if (mode == GovernanceMode.AdminOnly) return 0;
        if (mode == GovernanceMode.TimelockShort) return TIMELOCK_SHORT;
        if (mode == GovernanceMode.TimelockModerate) return TIMELOCK_MODERATE;
        return TIMELOCK_LONG;
    }

    /**
     * @notice Check if quorum is reached
     */
    function _quorumReached(uint256 proposalId) internal view returns (bool) {
        Proposal storage proposal = proposals[proposalId];
        
        if (proposal.mode == GovernanceMode.AdminOnly || 
            proposal.mode == GovernanceMode.TimelockShort) {
            return true; // No quorum required
        }
        
        uint256 totalVotes = proposal.forVotes + proposal.againstVotes + proposal.abstainVotes;
        uint256 totalSupply = assetRegistry.getValidators().length;
        
        return (totalVotes * 100) / totalSupply >= quorumNumerator;
    }

    /**
     * @notice Check if vote succeeded
     */
    function _voteSucceeded(uint256 proposalId) internal view returns (bool) {
        Proposal storage proposal = proposals[proposalId];
        return proposal.forVotes > proposal.againstVotes;
    }

    /**
     * @notice Get voting power
     */
    function _getVotes(address account) internal view returns (uint256) {
        return assetRegistry.isValidator(account) ? 1 : 0;
    }

    // Admin functions
    
    function setVotingDelay(uint256 newVotingDelay) external onlyRole(DEFAULT_ADMIN_ROLE) {
        votingDelay = newVotingDelay;
    }

    function setVotingPeriod(uint256 newVotingPeriod) external onlyRole(DEFAULT_ADMIN_ROLE) {
        votingPeriod = newVotingPeriod;
    }

    function setQuorumNumerator(uint256 newQuorumNumerator) external onlyRole(DEFAULT_ADMIN_ROLE) {
        require(newQuorumNumerator <= 100, "Invalid quorum");
        quorumNumerator = newQuorumNumerator;
    }

    function approveProposalJurisdiction(uint256 proposalId) external {
        bytes32 jurisdictionId = proposalJurisdictionIds[proposalId];
        require(jurisdictionId != bytes32(0), "Jurisdiction not derived");
        require(proposalJurisdictionReviewRequired[proposalId], "Review not required");
        bytes32 transitionJurisdictionId = proposalTransitionJurisdictionIds[proposalId];
        bool isPrimaryAuthority = assetRegistry.isGovernanceAuthority(jurisdictionId, msg.sender);
        bool isTransitionAuthority =
            transitionJurisdictionId != bytes32(0) &&
            transitionJurisdictionId != jurisdictionId &&
            assetRegistry.isGovernanceAuthority(transitionJurisdictionId, msg.sender);
        require(isPrimaryAuthority || isTransitionAuthority, "Not jurisdiction authority");

        bool approved;
        if (isPrimaryAuthority && !_proposalJurisdictionApprovals[proposalId][msg.sender]) {
            _proposalJurisdictionApprovals[proposalId][msg.sender] = true;
            proposalJurisdictionApprovalCount[proposalId] += 1;
            proposalLastJurisdictionApprover[proposalId] = msg.sender;
            emit ProposalJurisdictionApproved(
                proposalId,
                jurisdictionId,
                msg.sender,
                proposalJurisdictionApprovalCount[proposalId]
            );
            approved = true;
        }

        if (isTransitionAuthority && !_proposalTransitionJurisdictionApprovals[proposalId][msg.sender]) {
            _proposalTransitionJurisdictionApprovals[proposalId][msg.sender] = true;
            proposalTransitionJurisdictionApprovalCount[proposalId] += 1;
            proposalLastTransitionJurisdictionApprover[proposalId] = msg.sender;
            emit ProposalJurisdictionApproved(
                proposalId,
                transitionJurisdictionId,
                msg.sender,
                proposalTransitionJurisdictionApprovalCount[proposalId]
            );
            approved = true;
        }

        require(approved, "Already approved");
    }

    function hasJurisdictionApproval(uint256 proposalId, address authority) external view returns (bool) {
        return _proposalJurisdictionApprovals[proposalId][authority];
    }

    function hasTransitionJurisdictionApproval(uint256 proposalId, address authority) external view returns (bool) {
        return _proposalTransitionJurisdictionApprovals[proposalId][authority];
    }

    function getProposalEta(uint256 proposalId) external view returns (uint256) {
        return proposals[proposalId].eta;
    }

    function _requireJurisdictionApprovalIfNeeded(uint256 proposalId) internal view {
        if (!proposalJurisdictionReviewRequired[proposalId]) {
            return;
        }

        require(proposalJurisdictionApprovalCount[proposalId] > 0, "Primary jurisdiction approval required");

        bytes32 transitionJurisdictionId = proposalTransitionJurisdictionIds[proposalId];
        bytes32 jurisdictionId = proposalJurisdictionIds[proposalId];
        if (transitionJurisdictionId != bytes32(0) && transitionJurisdictionId != jurisdictionId) {
            require(
                proposalTransitionJurisdictionApprovalCount[proposalId] > 0,
                "Transition jurisdiction approval required"
            );
        }
    }
}
