This is trickier than it seemed! Here is some code that seems to do what you want, I think. It only adds lines at the beginning of a heading, and at the end of what I call the heading content (not including subtrees). It adds different numbers of lines at the end depending on what the next heading is.
I briefly tried adding it to the org-cycle-hook, and it seems to work ok. I would make sure you have a backup on files you use this on, since it changes your file! While developing it, I noticed some occasional visibility issues, and issues with the org-element-cache sometimes, and occasionally had to reset it. This version does not seem to have an issue there, but if you see weird behavior it might be related.
(defvar org-blank-lines-after-heading 1
"Number of blank lines to separate a heading from the content.")
(defvar org-blank-lines-after-content (cons 2 4)
"Cons cell for the number of blank lines after content in a heading.
The car is for when the next heading is at the same level, and
the cdr is for when the next heading is at a different level.
This is for the body specific to the headline, not counting
(defun org-format-heading-blank-lines ()
"Make sure each headline has exactly
`org-blank-lines-after-heading' after the heading, and
`org-blank-lines-after-content' blank lines at the end of its
content. Only works when point is in a headline."
(let ((current-level (nth 0 (org-heading-components)))
;; chomp blank lines then add what you want back.
(while (and (not (eobp)) (looking-at "^[[:space:]]*$"))
(insert (cl-loop for i from 0 below org-blank-lines-after-heading concat "\n")))
;; Now go to the end of content and insert lines if needed.
(setq next-level (nth 0 (org-heading-components)))
;; chomp lines back then reinsert them.
(while (looking-at "^[[:space:]]*$")
(unless (eobp) (end-of-line))
(insert (cl-loop for i from 0 below (if (= current-level next-level)
(defun org-format-headings (arg)
(while (re-search-forward org-heading-regexp nil t)