rules_version = '2';
service cloud.firestore {
  match /databases/{databaseId}/documents {
    function valueExists(object, property) {
      return (property in object) && object[property] != null;
    }

    function isEqualOptional(object0, property0, object1, property1) {
      return valueExists(object0, property0)
        ? (valueExists(object1, property1) && object0[property0] == object1[property1])
        : !valueExists(object1, property1);
    }

    function isAuthenticated() {
      return request.auth != null 
        && ('type' in request.auth.token)
        && (
          !valueExists(request.auth.token, 'disabled')
          || request.auth.token.disabled == false
        );
    }

    function isAdmin() {
      return isAuthenticated()
        && request.auth.token.type == 'admin';
    }

    function isPartOf(organizationId) {
      return ('organization' in request.auth.token)
        && request.auth.token.organization == organizationId;
    }

    function isOwnerOf(organizationId) {
      return organizationId != null
        && isAuthenticated()
        && request.auth.token.type == 'owner'
        && isPartOf(organizationId);
    }

    function isOwnerOrClinicianOf(organizationId) {
      return organizationId != null
        && isAuthenticated()
        && request.auth.token.type in ['owner', 'clinician']
        && isPartOf(organizationId);
    }

    function isUser(userId) {
      return isAuthenticated()
        && request.auth.uid == userId;
    }

    function getUser(userId) {
      return get(/databases/$(databaseId)/documents/users/$(userId));
    }

    function getInvitation(invitationId) {
      return get(/databases/$(databaseId)/documents/invitations/$(invitationId));
    }

    match /invitations/{invitationId} {
      function securityRelatedFieldsDidNotChange() {
        return isEqualOptional(request.resource.data.user, 'type', resource.data.user, 'type')
          && isEqualOptional(request.resource.data.user, 'organization', resource.data.user, 'organization')
      }

      allow read, delete: if isAdmin() || isOwnerOrClinicianOf(resource.data.user.organization);
      allow create: if isAdmin();
      allow update: if isAdmin() || (securityRelatedFieldsDidNotChange() && isOwnerOrClinicianOf(resource.data.user.organization));
    }


    match /invitations/{invitationId}/{collectionName}/{documentId} {
      function isOwnerOrClinicianOfSameOrganization() {
        let invitation = getInvitation(invitationId);
        return invitation != null && isOwnerOrClinicianOf(invitation.data.user.organization);
      }

      function isPatientWritableCollectionName() {
        return collectionName.matches('^[A-Za-z]+Observations$')
          || collectionName in ['questionnaireResponses']
      }

      function isClinicianWritableCollectionName() {
        return isPatientWritableCollectionName()
          || collectionName in ['allergyIntolerances', 'appointments', 'medicationRequests'];
      }

      allow read: if isAdmin() 
        || isOwnerOrClinicianOfSameOrganization();

      allow write: if isAdmin() 
        || (isOwnerOrClinicianOfSameOrganization() && isClinicianWritableCollectionName());
    }

    match /medicationClasses/{medicationClassId} {
      allow read: if isAuthenticated();
      allow write: if isAdmin();
    }

    match /medications/{documents=**} {
      allow read: if isAuthenticated();
      allow write: if isAdmin();
    }

    match /organizations/{organizationId} {
      allow read: if isAdmin() || isPartOf(organizationId);
      allow update: if isAdmin() || isOwnerOf(organizationId);
      allow create, delete: if isAdmin();
    }

    match /questionnaires/{questionnaireId} {
      allow read: if isAuthenticated();
      allow write: if isAdmin();
    }

    match /users/{userId} {
      function securityRelatedFieldsDidNotChange() {
        return isEqualOptional(request.resource.data, 'type', resource.data, 'type')
          && isEqualOptional(request.resource.data, 'disabled', resource.data, 'disabled')
          && isEqualOptional(request.resource.data, 'organization', resource.data, 'organization');
      }

      function isAllowedUpdateWithinOrganization() {
        return valueExists(resource.data, 'type') ? 
          (
            resource.data.type in ['patient']
            ? isOwnerOf(resource.data.organization)
            : isOwnerOrClinicianOf(resource.data.organization) && resource.data.type in ['clinician']
          ) : false;
      }

      allow read: if isAdmin()
        || (request.auth != null && request.auth.uid == userId)
        || (resource == null && isAuthenticated())
        || (resource != null && valueExists(resource.data, 'organization') && isOwnerOrClinicianOf(resource.data.organization));

      allow create: if isAdmin()
        || (isUser(userId) && !valueExists(request.resource.data, 'organization') && !valueExists(request.resource.data, 'type'));

      allow update: if isAdmin()
        || (securityRelatedFieldsDidNotChange() && (isUser(userId) || isAllowedUpdateWithinOrganization()));

      allow delete: if isAdmin();
    }

    match /users/{userId}/{collectionName}/{documentId} {
      function isOwnerOrClinicianOfSameOrganization() {
        let userDoc = getUser(userId);
        return (userDoc.data != null)
          && ('organization' in userDoc.data) 
          && isOwnerOrClinicianOf(userDoc.data.organization);
      }

      function isPatientWritableCollectionName() {
        return collectionName.matches('^[A-Za-z]+Observations$')
          || collectionName in ['questionnaireResponses']
      }

      function isClinicianWritableCollectionName() {
        return isPatientWritableCollectionName()
          || collectionName in ['allergyIntolerances', 'appointments', 'medicationRequests'];
      }

      allow read: if isAdmin()
        || (request.auth != null && request.auth.uid == userId)
        || isOwnerOrClinicianOfSameOrganization();

      allow write: if isAdmin() 
        || (isUser(userId) && isPatientWritableCollectionName())
        || (isOwnerOrClinicianOfSameOrganization() && isClinicianWritableCollectionName());
    }

    match /videoSections/{documents=**} {
      allow read: if isAuthenticated();
      allow write: if isAdmin();
    }
  }
}