Create a structured reply to a ticket.
    
    :param int ticket_id: The ticket to reply to.
    :param content: ``str``, ``dict``, or iterator of ``(title, content``) pairs.
    :returns: ``(reply_id, reply_content)``.

def reply_to_ticket(ticket_id, content, user_id=None):
    """Create a structured reply to a ticket.
    
    :param int ticket_id: The ticket to reply to.
    :param content: ``str``, ``dict``, or iterator of ``(title, content``) pairs.
    :returns: ``(reply_id, reply_content)``.
    
    """
    
    user_comment = None
    exc_type = None

    if isinstance(content, dict):
        content = sorted(content.iteritems())

    if not isinstance(content, basestring):
        parts = []
        for heading, part in content:
            
            # This one gets special cased into a Reply.
            if heading.lower() == 'user comment':
                user_comment = part

            # Mappings.
            if isinstance(part, dict):
                parts.append((heading, '\n'.join('%s = %r' % x
                    for x in sorted(part.iteritems())
                )))
            
            # Iterators.
            elif isinstance(part, (list, tuple)):
                
                # Exceptions.
                if (len(part) == 3 and
                    issubclass(part[0], Exception) and
                    isinstance(part[1], Exception) and
                    isinstance(part[2], types.TracebackType)
                ):
                    exc_type = part[0]
                    parts.append((heading, '\n'.join(traceback.format_exception(*part)).rstrip()))
                
                # Iterators.
                else:
                    parts.append((heading, '\n'.join('- %r' % x for x in part)))
            
            # The rest.
            else:
                parts.append((heading, str(part)))
        
        # Join the parts.
        content = '\n\n'.join('%s\n%s\n%s' % (heading, '=' * len(heading), part) for heading, part in parts)
    
    # Get the Shotgun and Project.
    # TODO: Somehow pass this in later.
    shotgun = get_shotgun()

    # Convert requested user ID to a login.
    if user_id:
        user = shotgun.find_one('HumanUser', [('id', 'is', user_id)], ['login'])
        if user:
            shotgun.config.sudo_as_login = user['login']

    # Make sure we do have a login.
    if not shotgun.config.sudo_as_login:
        user = shotgun.find_one('HumanUser', [('email', 'starts_with', os.getlogin() + '@')], ['login'])
        if user:
            shotgun.config.sudo_as_login = user['login']
    
    reply = None

    if user_comment:
        reply = shotgun.create('Reply', {
            'content': user_comment,
            'entity': {'type': 'Ticket', 'id': ticket_id},
        })

    if content.strip():
        with tempfile.NamedTemporaryFile(suffix='ticket-%d-reply.txt' % ticket_id) as fh:
            fh.write(content)
            fh.flush()
            shotgun.upload('Ticket', ticket_id, fh.name, 'attachments',
               display_name='Ticket-%d.%s.%s.txt' % (
                    ticket_id,
                    exc_type.__name__ if exc_type else 'details',
                    datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
                ),
            )

    return reply and reply['id'], content