This works with gnu sed, I don't think it relies on any gnu-specific extensions but I don't know.
echo "$yourdata" | sed -ne '1{h;d}; /^TAG1$/ {n; /^TAG2$/{n;N;N; /\nTAG3$/ {s///; H; n;N;N; /\nTAG4$/ {s///; H; g; s/\n\n/-/gp; q; } } } }'
Result: alfa-bravo-charlie
How does it work? First we tell sed "-n" we want not to print anything unless we specifically say [p]rint.
The first block of the sed expression is "1{h;d}". This says when we read line 1, stash that line in the [h]old buffer then [d]elete it from the working buffer so that we'll read the next line and pass it through the sed expression from the start.
When reading subsequent lines the "1{...}" block will be skipped.
We don't match anything further until we hit the line TAG1. At this point we execute the long {...} block. This says first read the [n]ext line, overwriting the TAG1 line which was in the buffer. If the buffer now is TAG2, then we execute the next inner {...} block. That first reads the [n]ext line, overwriting what's already in the buffer. The next two commands are "N;N". This means read the next 2 lines but append them to the work buffer, rather than overwriting it. If the work buffer now matches /\nTAG3$/, then we execute the next inner {...} block. That says first "s///", in other words substitute the empty string for the most-recently matched expression. This deletes the "\nTAG3" from the end of the working buffer, leaving "\nbravo". Then we do [H], which appends that to the hold buffer. ([h] overwrites the hold buffer, [H] appends to it). So now the hold buffer contains the first line "alfa", then the next line "\nbravo". These are joined by a newline, so we've really got "alfa\n\nbravo." We'll take care of the two newlines later.
We keep going until we've got "alfa\n\nbravo\n\ncharly" in the hold buffer. Then we say [g]et the hold buffer (overwriting whatever is in the working buffer). We do a "s/\n\n/-/" on this to turn the double newlines into dashes. We add "g" and "p" flags to the end of the [s] command so that the substitution works globally (i.e. doesn't just do one substitution then stop) and that the result after substitution is [p]rinted.
Then we [q]uit, we don't need to read the rest of the input stream.