"""Utilities for strict, flat tuples of URL/filename-safe strings.

This module defines SafeStrTuple, an immutable, hashable, flat tuple of non-empty
strings restricted to a predefined safe character set and bounded length. It is
useful for constructing keys and paths that must be portable and safe for URLs
and filesystems.
"""
from __future__ import annotations
from collections.abc import Iterator, Sequence, Mapping, Hashable
from typing import Any, Self
from .safe_chars import SAFE_CHARS_SET, SAFE_STRING_MAX_LENGTH


def _is_sequence_not_mapping(obj: Any) -> bool:
    """Return True if the object looks like a sequence but not a mapping.

    This function prefers ABC checks but falls back to duck-typing to handle
    some custom/typed collections.

    Args:
        obj: Object to inspect.

    Returns:
        True if obj is a sequence (e.g., list, tuple) and not a mapping
        (e.g., dict); otherwise False.
    """
    if isinstance(obj, Sequence) and not isinstance(obj, Mapping):
        return True
    elif hasattr(obj, "keys") and callable(obj.keys):
        return False
    elif (
        hasattr(obj, "__getitem__")
        and callable(obj.__getitem__)
        and hasattr(obj, "__len__")
        and callable(obj.__len__)
        and hasattr(obj, "__iter__")
        and callable(obj.__iter__)
    ):
        return True
    else:
        return False


class SafeStrTuple(Sequence[str], Hashable):
    """An immutable sequence of URL/filename-safe strings.

    The sequence is flat (no nested structures) and hashable, making it suitable
    for use as a dictionary key. All strings are validated to contain only
    characters from SAFE_CHARS_SET and to have length less than
    SAFE_STRING_MAX_LENGTH.
    """

    strings: tuple[str, ...]

    def __init__(self, *args, **kwargs):
        """Initialize from strings or nested sequences of strings.

        The constructor accepts zero or more arguments which may be:
        - a SafeStrTuple
        - a single string
        - a sequence (list/tuple/etc.) containing any of the above recursively

        The input is flattened left-to-right into a single tuple of validated
        strings. Empty strings and strings with characters outside
        SAFE_CHARS_SET are rejected. Strings must also be shorter than
        SAFE_STRING_MAX_LENGTH.

        Args:
            *args: Zero or more inputs (strings, sequences, or SafeStrTuple) that
                will be flattened into a tuple of safe strings.
            **kwargs: Not supported.

        Raises:
            TypeError: If unexpected keyword arguments are provided or if an
                argument has an invalid type.
            ValueError: If a string is empty, too long, contains disallowed
                characters, or is '.' or '..' (which have special filesystem
                semantics and are not valid key components).
        """
        if len(kwargs) != 0:
            raise TypeError(f"Unexpected keyword arguments: {list(kwargs.keys())}")
        candidate_strings = []
        for a in args:
            if isinstance(a, SafeStrTuple):
                candidate_strings.extend(a.strings)
            elif isinstance(a, str):
                if len(a) == 0:
                    raise ValueError("Strings must be non-empty")
                if a in (".", ".."):
                    raise ValueError(
                        f"'{a}' is not allowed as a key component")
                if len(a) >= SAFE_STRING_MAX_LENGTH:
                    raise ValueError(
                        f"String length must be < {SAFE_STRING_MAX_LENGTH}, got {len(a)}")
                if not all(c in SAFE_CHARS_SET for c in a):
                    raise ValueError("String contains disallowed characters")
                candidate_strings.append(a)
            elif _is_sequence_not_mapping(a):
                if len(a) > 0:
                    candidate_strings.extend(SafeStrTuple(*a).strings)
            else:
                raise TypeError(f"Invalid argument type: {type(a)}")
        self.strings = tuple(candidate_strings)

    @property
    def str_chain(self) -> tuple[str, ...]:
        """Alias for strings for backward compatibility.

        Returns:
            The underlying tuple of strings.
        """
        return self.strings

    def __getitem__(self, key: int) -> str:
        """Return the string at the given index.

        Args:
            key: Zero-based index.

        Returns:
            The string at the specified position.
        """
        return self.strings[key]

    def __len__(self) -> int:
        """Return the number of strings in the tuple.

        Returns:
            The number of elements.
        """
        return len(self.strings)

    def __hash__(self) -> int:
        """Compute the hash of the underlying tuple.

        Returns:
            A hash value suitable for dict/set usage.
        """
        return hash(self.strings)

    def __repr__(self) -> str:
        """Return a developer-friendly representation.

        Returns:
            A representation including the class name and contents.
        """
        return f"{type(self).__name__}({self.strings})"

    def __eq__(self, other) -> bool:
        """Compare two SafeStrTuple-compatible objects for equality.

        If other is not a SafeStrTuple, it will be coerced using the same
        validation rules. If coercion fails, returns NotImplemented to allow
        Python to try the reverse comparison.

        Args:
            other: Another SafeStrTuple or compatible input.

        Returns:
            True if both contain the same sequence of strings.
        """
        if isinstance(other, SafeStrTuple):
            if type(self).__eq__ != type(other).__eq__:
                return other.__eq__(self)
        else:
            try:
                other = SafeStrTuple(other)
            except (TypeError, ValueError):
                return NotImplemented

        return self.strings == other.strings

    def __add__(self, other) -> SafeStrTuple:
        """Concatenate with another SafeStrTuple-compatible object.

        Args:
            other: Another SafeStrTuple or compatible input.

        Returns:
            A new instance containing elements of self then other.
        """
        other = SafeStrTuple(other)
        return SafeStrTuple(*(self.strings + other.strings))

    def __radd__(self, other) -> SafeStrTuple:
        """Concatenate with another object in reversed order (other + self).

        Args:
            other: Another SafeStrTuple or compatible input.

        Returns:
            A new instance containing elements of other then self.
        """
        other = SafeStrTuple(other)
        return SafeStrTuple(*(other.strings + self.strings))

    def __iter__(self) -> Iterator[str]:
        """Return an iterator over the strings.

        Returns:
            An iterator over the internal tuple.
        """
        return iter(self.strings)

    def __contains__(self, item: object) -> bool:
        """Check membership.

        Args:
            item: String to check for presence.

        Returns:
            True if item is present.
        """
        return item in self.strings

    def __reversed__(self) -> SafeStrTuple:
        """Return a reversed SafeStrTuple.

        Returns:
            A new instance with elements in reverse order.
        """
        return SafeStrTuple(*reversed(self.strings))

    def __copy__(self) -> Self:
        """Return self since immutable objects need no copying."""
        return self

    def __deepcopy__(self, memo: dict[int, Any]) -> Self:
        """Return self since immutable objects need no deep copying."""
        return self

class NonEmptySafeStrTuple(SafeStrTuple):
    """A SafeStrTuple that must contain at least one string.

    This subclass enforces that the tuple is non-empty.
    """

    def __init__(self, *args, **kwargs):
        """Initialize and enforce non-empty constraint.

        Args:
            *args: One or more inputs (strings, sequences, or SafeStrTuple) that
                will be flattened into a tuple of safe strings.
            **kwargs: Not supported.

        Raises:
            TypeError: If unexpected keyword arguments are provided, if no args
                are provided, or if an argument has an invalid type.
            ValueError: If a string is empty, too long, contains disallowed
                characters, or if the resulting tuple is empty.
        """
        super().__init__(*args, **kwargs)
        if len(self.strings) == 0:
            raise ValueError("NonEmptySafeStrTuple must contain at least one string")