contract MultiSigAdministration {
  event TenantRegistered(
    address indexed tenant,
    address[] creators,
    address[] admins,
    uint8 quorum
  );
  event ActionInitiated(address indexed tenant, uint256 indexed id, address initiatedBy);
  event ActionApproved(address indexed tenant, uint256 indexed id, address approvedBy);
  event ActionRejected(address indexed tenant, uint256 indexed id, address rejectedBy);
  event ActionCompleted(address indexed tenant, uint256 indexed id);
  event ActionFailed(address indexed tenant, uint256 indexed id);

  using MultiSigAction for MultiSigAction.Action;

  enum AdminAction {ADD_ADMIN, REMOVE_ADMIN, CHANGE_QUORUM, ADD_CREATOR, REMOVE_CREATOR}
  uint8 private constant OTHER_ACTION = uint8(AdminAction.REMOVE_CREATOR) + 1;

  mapping(address => uint256) public numOfActions;
  mapping(address => mapping(address => bool)) public isAdmin;
  mapping(address => uint8) public numOfAdmins;
  mapping(address => mapping(address => bool)) public isCreator;
  mapping(address => uint8) public quorums;
  mapping(address => bool) public isRegistered;
  mapping(address => uint256) public minValidActionId;

  mapping(address => mapping(uint256 => MultiSigAction.Action)) private actions;

  modifier onlyAdminOf(address _tenant) {
    require(isAdmin[_tenant][msg.sender], "ONLY_ADMIN_OF_TENANT");

    _;
  }

  modifier onlyAdminOrCreatorOf(address _tenant) {
    require(
      isAdmin[_tenant][msg.sender] || isCreator[_tenant][msg.sender],
      "ONLY_ADMIN_OR_CREATOR_OF_TENANT"
    );

    _;
  }

  modifier onlyRegistered(address _tenant) {
    require(isRegistered[_tenant], "ONLY_REGISTERED_TENANT");

    _;
  }

  modifier onlyMe() {
    require(msg.sender == address(this), "ONLY_INTERNAL");

    _;
  }

  modifier onlyExistingAction(address _tenant, uint256 _id) {
    require(_id <= numOfActions[_tenant], "ONLY_EXISTING_ACTION");
    require(_id > 0, "ONLY_EXISTING_ACTION");

    _;
  }

  constructor() public {}

  /* Public Functions - Start */
  function register(
    address _tenant,
    address[] memory _creators,
    address[] memory _admins,
    uint8 _quorum
  ) public returns (bool success) {
    require(
      msg.sender == _tenant || msg.sender == Ownable(_tenant).owner(),
      "ONLY_TENANT_OR_TENANT_OWNER"
    );

    return _register(_tenant, _creators, _admins, _quorum);
  }

  function initiateAdminAction(
    address _tenant,
    AdminAction _adminAction,
    bytes memory _callbackData
  ) public onlyRegistered(_tenant) onlyAdminOf(_tenant) returns (uint256 id) {
    string memory _callbackSig = _getAdminActionCallbackSig(_adminAction);

    uint256 _id = _initiateAction(
      uint8(_adminAction),
      _tenant,
      address(this),
      _callbackSig,
      abi.encodePacked(abi.encode(_tenant), _callbackData)
    );
    _approveAction(_tenant, _id);

    return _id;
  }

  function initiateAction(address _tenant, string memory _callbackSig, bytes memory _callbackData)
    public
    onlyRegistered(_tenant)
    onlyAdminOrCreatorOf(_tenant)
    returns (uint256 id)
  {
    uint256 _id = _initiateAction(OTHER_ACTION, _tenant, _tenant, _callbackSig, _callbackData);

    if (isAdmin[_tenant][msg.sender]) {
      _approveAction(_tenant, _id);
    }

    return _id;
  }

  function approveAction(address _tenant, uint256 _id)
    public
    onlyRegistered(_tenant)
    onlyAdminOf(_tenant)
    onlyExistingAction(_tenant, _id)
    returns (bool success)
  {
    return _approveAction(_tenant, _id);
  }

  function rejectAction(address _tenant, uint256 _id)
    public
    onlyRegistered(_tenant)
    onlyAdminOrCreatorOf(_tenant)
    onlyExistingAction(_tenant, _id)
    returns (bool success)
  {
    return _rejectAction(_tenant, _id);
  }

  function addAdmin(address _tenant, address _admin, bool _increaseQuorum) public onlyMe {
    minValidActionId[_tenant] = numOfActions[_tenant] + 1;
    _addAdmin(_tenant, _admin);

    if (_increaseQuorum) {
      uint8 _quorum = quorums[_tenant];
      uint8 _newQuorum = _quorum + 1;
      require(_newQuorum > _quorum, "OVERFLOW");

      _changeQuorum(_tenant, _newQuorum);
    }
  }

  function removeAdmin(address _tenant, address _admin, bool _decreaseQuorum) public onlyMe {
    uint8 _quorum = quorums[_tenant];

    if (_decreaseQuorum && _quorum > 1) {
      _changeQuorum(_tenant, _quorum - 1);
    }

    minValidActionId[_tenant] = numOfActions[_tenant] + 1;
    _removeAdmin(_tenant, _admin);
  }

  function changeQuorum(address _tenant, uint8 _quorum) public onlyMe {
    minValidActionId[_tenant] = numOfActions[_tenant] + 1;
    _changeQuorum(_tenant, _quorum);
  }

  function addCreator(address _tenant, address _creator) public onlyMe {
    _addCreator(_tenant, _creator);
  }

  function removeCreator(address _tenant, address _creator) public onlyMe {
    _removeCreator(_tenant, _creator);
  }

  function getAction(address _tenant, uint256 _id)
    public
    view
    returns (
    bool isAdminAction,
    string memory callbackSig,
    bytes memory callbackData,
    uint8 quorum,
    address requestedBy,
    address rejectedBy,
    uint8 numOfApprovals,
    bool rejected,
    bool failed,
    bool completed,
    bool valid
  )
  {
    MultiSigAction.Action storage _action = _getAction(_tenant, _id);

    isAdminAction = _action.callbackAddress == address(this);
    callbackSig = _action.callbackSig;
    callbackData = _action.callbackData;
    quorum = _action.quorum;
    requestedBy = _action.requestedBy;
    rejectedBy = _action.rejectedBy;
    numOfApprovals = _action.numOfApprovals;
    rejected = _action.rejected;
    failed = _action.failed;
    completed = _action.isCompleted();
    valid = _isActionValid(_tenant, _id);
  }

  function hasApprovedBy(address _tenant, uint256 _id, address _admin)
    public
    view
    returns (bool approvedBy)
  {
    approvedBy = _getAction(_tenant, _id).approvedBy[_admin];
  }
  /* Public Functions - End */

  /* Private Functions - Start */
  function _getAction(address _tenant, uint256 _id)
    private
    view
    returns (MultiSigAction.Action storage)
  {
    return actions[_tenant][_id];
  }

  function _isActionValid(address _tenant, uint256 _id) private view returns (bool) {
    return _id >= minValidActionId[_tenant];
  }

  function _getAdminActionCallbackSig(AdminAction _adminAction)
    private
    pure
    returns (string memory)
  {
    if (_adminAction == AdminAction.ADD_ADMIN) {
      return "addAdmin(address,address,bool)";
    }

    if (_adminAction == AdminAction.REMOVE_ADMIN) {
      return "removeAdmin(address,address,bool)";
    }

    if (_adminAction == AdminAction.CHANGE_QUORUM) {
      return "changeQuorum(address,uint8)";
    }

    if (_adminAction == AdminAction.ADD_CREATOR) {
      return "addCreator(address,address)";
    }

    return "removeCreator(address,address)";
  }

  function _addCreator(address _tenant, address _creator) private {
    require(_creator != address(this), "INVALID_CREATOR");
    require(!isAdmin[_tenant][_creator], "ALREADY_ADMIN");
    require(!isCreator[_tenant][_creator], "ALREADY_CREATOR");

    isCreator[_tenant][_creator] = true;
  }

  function _removeCreator(address _tenant, address _creator) private {
    require(isCreator[_tenant][_creator], "NOT_CREATOR");

    isCreator[_tenant][_creator] = false;
  }

  function _addAdmin(address _tenant, address _admin) private {
    require(_admin != address(this), "INVALID_ADMIN");
    require(!isAdmin[_tenant][_admin], "ALREADY_ADMIN");
    require(!isCreator[_tenant][_admin], "ALREADY_CREATOR");
    require(numOfAdmins[_tenant] + 1 > numOfAdmins[_tenant], "OVERFLOW");

    numOfAdmins[_tenant]++;
    isAdmin[_tenant][_admin] = true;
  }

  function _removeAdmin(address _tenant, address _admin) private {
    require(isAdmin[_tenant][_admin], "NOT_ADMIN");
    require(--numOfAdmins[_tenant] >= quorums[_tenant], "TOO_FEW_ADMINS");

    isAdmin[_tenant][_admin] = false;
  }

  function _changeQuorum(address _tenant, uint8 _quorum) private {
    require(_quorum <= numOfAdmins[_tenant], "QUORUM_TOO_BIG");
    require(_quorum > 0, "QUORUM_ZERO");

    quorums[_tenant] = _quorum;
  }

  function _register(
    address _tenant,
    address[] memory _creators,
    address[] memory _admins,
    uint8 _quorum
  ) private returns (bool) {
    require(_tenant != address(this), "INVALID_TENANT");
    require(!isRegistered[_tenant], "ALREADY_REGISTERED");

    for (uint8 i = 0; i < _admins.length; i++) {
      _addAdmin(_tenant, _admins[i]);
    }
    _changeQuorum(_tenant, _quorum);

    for (uint8 i = 0; i < _creators.length; i++) {
      _addCreator(_tenant, _creators[i]);
    }

    isRegistered[_tenant] = true;
    emit TenantRegistered(_tenant, _creators, _admins, _quorum);

    return true;
  }

  function _initiateAction(
    uint8 _actionType,
    address _tenant,
    address _callbackAddress,
    string memory _callbackSig,
    bytes memory _callbackData
  ) private returns (uint256) {
    uint256 _id = ++numOfActions[_tenant];
    uint8 _quorum = quorums[_tenant];

    if (_actionType == uint8(AdminAction.REMOVE_ADMIN)) {
      require(numOfAdmins[_tenant] > 1, "TOO_FEW_ADMINS");

      if (_quorum == numOfAdmins[_tenant] && _quorum > 2) {
        _quorum = numOfAdmins[_tenant] - 1;
      }
    }

    _getAction(_tenant, _id).init(
      _actionType,
      _callbackAddress,
      _callbackSig,
      _callbackData,
      _quorum
    );

    emit ActionInitiated(_tenant, _id, msg.sender);

    return _id;
  }

  function _approveAction(address _tenant, uint256 _id) private returns (bool) {
    require(_isActionValid(_tenant, _id), "ACTION_INVALIDATED");

    MultiSigAction.Action storage _action = _getAction(_tenant, _id);
    _action.approve();
    emit ActionApproved(_tenant, _id, msg.sender);

    if (_action.isCompleted()) {
      _action.complete();

      if (_action.failed) {
        emit ActionFailed(_tenant, _id);
      } else {
        emit ActionCompleted(_tenant, _id);
      }
    }

    return true;
  }

  function _rejectAction(address _tenant, uint256 _id) private returns (bool) {
    MultiSigAction.Action storage _action = _getAction(_tenant, _id);

    if (isCreator[_tenant][msg.sender]) {
      require(msg.sender == _action.requestedBy, "CREATOR_REJECT_NOT_REQUESTOR");
    }

    if (_action.actionType == uint8(AdminAction.REMOVE_ADMIN)) {
      (, address _admin, ) = abi.decode(_action.callbackData, (address, address, bool));

      require(_admin != msg.sender, "CANNOT_REJECT_ITS_OWN_REMOVAL");
    }

    _action.reject();

    emit ActionRejected(_tenant, _id, msg.sender);

    return true;
  }
  /* Private Functions - End */
}
function register(
    address _tenant,
    address[] memory _creators,
    address[] memory _admins,
    uint8 _quorum
  ) public returns (bool success) {
    require(
      msg.sender == _tenant || msg.sender == Ownable(_tenant).owner(),
      "ONLY_TENANT_OR_TENANT_OWNER"
    );

    return _register(_tenant, _creators, _admins, _quorum);
  }
/* Public Functions - Start */