from typing import Type
from typing import Iterable
from typing import Optional
from textwrap import dedent


def get_docstring(
    cls: Type,
    *,
    flatten: bool = False,
    fallback_to_ancestors: bool = False,
    ignored_ancestors: Iterable[Type] = (object,),
) -> Optional[str]:
    """Get the docstring for a class with properly handled indentation.

    :param cls: the class, e.g. MyClass.
    :param flatten: flatten the docstring into a single line by replacing all newlines with spaces
        and stripping leading indentation.
    :param fallback_to_ancestors: if the class does not have docstring defined, try to use docstring
        from its superclasses, if any. This traverses in the MRO order, i.e. tries to use its
        direct parent, then grandparent, and ultimately `object()`.
    :param ignored_ancestors: if `fallback_to_ancestors` is True, do not use the docstring from
        these ancestors.
    """
    if cls.__doc__ is not None:
        docstring = cls.__doc__.strip()
    else:
        if not fallback_to_ancestors:
            return None
        # Fallback to ancestors in MRO order.
        ancestor_docstring = next(
            (
                ancestor_cls.__doc__.strip()
                for ancestor_cls in cls.mro()[1:]
                if ancestor_cls not in ignored_ancestors and ancestor_cls.__doc__ is not None
            ),
            None,
        )
        if ancestor_docstring is None:
            return None
        docstring = ancestor_docstring

    newline_index = docstring.find("\n")
    if newline_index == -1:
        return docstring

    # Fix indentation of lines after the first line.
    lines = [docstring[:newline_index], *dedent(docstring[newline_index + 1 :]).splitlines()]

    if flatten:
        return " ".join(line.strip() for line in lines if line).strip()
    return "\n".join(lines)