"""
Helper functions for generating human readable text.
"""

from __future__ import annotations

import contextlib
import functools
import io
import typing

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Iterable, Sequence
    from typing import Any

    from pydantic import BaseModel


def join_with_and(
    items: Iterable[Any],
    *,
    quote: bool = True,
    sort: bool = True,
    fallback_string: str = "(none)",
) -> str:
    """
    Join items with "and" as the last joiner.
    On empty input, returns the fallback string.

    Arguments
    ---------
    items : Iterable[Any]
        The items to join.
    quote : bool
        Whether to quote the items.
    sort : bool
        Whether to sort the items.
    fallback_string : str
        The string to return if there are no items.
    """
    return _join(
        items=items,
        quoting=_quote if quote else str,
        last_joiner=" and ",
        sort=sort,
        fallback_string=fallback_string,
    )


def join_with_or(
    items: Iterable[Any],
    *,
    quote: bool = True,
    sort: bool = True,
    fallback_string: str = "(none)",
) -> str:
    """
    Join items with "or" as the last joiner.
    On empty input, returns the fallback string.

    Arguments
    ---------
    items : Iterable[Any]
        The items to join.
    quote : bool
        Whether to quote the items.
    sort : bool
        Whether to sort the items.
    fallback_string : str
        The string to return if there are no items.
    """
    return _join(
        items=items,
        quoting=_quote if quote else str,
        last_joiner=" or ",
        sort=sort,
        fallback_string=fallback_string,
    )


def _join(
    *,
    items: Iterable[Any],
    quoting: Callable[[Any], str],
    last_joiner: str,
    sort: bool,
    fallback_string: str,
):
    """Internal helper for :func:`join_with_and` and :func:`join_with_or`."""
    items_iter = map(quoting, items)
    if sort:
        items_iter = sorted(items_iter)
    if result := join(
        items_iter,
        last_joiner=last_joiner,
    ):
        return result
    return fallback_string


def join(
    items: Iterable[str],
    *,
    separator: str = ", ",
    last_joiner: str = ", ",
):
    """
    Join items with a separator, with a different last joiner.

    Arguments
    ---------
    items : Iterable[str]
        The items to join.

    Keyword Arguments
    -----------------
    separator : str
        The separator to use between items, by default ``, ``.
    last_joiner : str
        The string to use before the last item.

    Returns
    -------
    str
        The joined string.
    """
    with io.StringIO() as buf:
        # join items with the specified separator
        last_item = None
        for item in iter(items):
            if buf.tell():
                buf.write(separator)
            if last_item is not None:
                buf.write(last_item)
            last_item = item

        # perform the last join
        if buf.tell():
            buf.write(last_joiner)
        if last_item is not None:
            buf.write(last_item)

        return buf.getvalue()


def _quote(item: Any) -> str:
    text = str(item)
    if "'" in text:
        return f'"{text}"'
    return f"'{text}'"


def get_context_name(loc: Sequence[str | int]) -> str:
    """Get the parent context name for a location."""
    if not isinstance(loc, tuple):
        loc = tuple(loc)
    return _get_context_name(loc)


@functools.lru_cache(16)
def _get_context_name(loc: tuple[str | int, ...]) -> str:
    with contextlib.suppress(StopIteration):
        # find the first string in the parents
        parent = next(filter(lambda x: isinstance(x, str), reversed(loc)))
        return f"the '{parent}' section"
    return "current context"


def get_alias(model: BaseModel, name: str) -> str:
    """Get the alias of a field in a model."""
    field = type(model).model_fields[name]
    return field.alias or name
