With zsh:
autoload zmv # best in ~/.zshrc
zmv -n '([^#]#)(\#*)(.*)' '$1${(j: :)${(os: :)2}}$3'
(remove the -n (dry-run) if happy).
[^#]#: 0 or more non-# characters (#is like*in regexps)s: :split on spaceo: order (sort)j: :: join with space.
So, we're splitting the part in between the first # (included) and last . (excluded) on space, sort the resulting list which we join back with space.
Recursively:
zmv -n '(**/)([^#]#)(\#*)(.*)' '$1$2${(j: :)${(os: :)3}}$4'
To allow spaces in tag names, we could split on # and trim trailing spaces, sort and join on # with:
zmv -n '([^#]#\#)(*)(.*)' '$1${(j: #:)${(os:#:)2}%% #}$3'
Add a (#qD) glob qualifier if you also want to process hidden files (Dot files) or want to process files in hidden directories.