"""Target resolution for commands that take entity/thing arguments."""

from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from mudlib.entity import Entity
from mudlib.thing import Thing

if TYPE_CHECKING:
    from mudlib.zone import Zone


def parse_target(raw: str) -> tuple[int, str]:
    """Parse ordinal prefix from target name.

    Examples:
        "goblin" -> (1, "goblin")
        "2.goblin" -> (2, "goblin")
        "10.rat" -> (10, "rat")

    Invalid ordinals (0, negative, non-numeric) are treated as part of the name.
    """
    if "." not in raw:
        return (1, raw)

    prefix, rest = raw.split(".", 1)
    try:
        ordinal = int(prefix)
        if ordinal >= 1:
            return (ordinal, rest)
    except ValueError:
        pass

    # Invalid ordinal, treat as plain name
    return (1, raw)


def resolve_target(
    name: str,
    candidates: list[Any],
    *,
    key: Callable[[Any], str] | None = None,
) -> Any | None:
    """Resolve a target by name from a list of candidates.

    Matching priority:
    1. Parse ordinal from name (e.g., "2.goblin")
    2. Exact match on name (case-insensitive)
    3. Prefix match on name (case-insensitive)
    4. Exact match on alias (case-insensitive, if candidate has aliases)
    5. Prefix match on alias (case-insensitive)
    6. Return Nth match based on ordinal

    Args:
        name: Target name to search for (may include ordinal prefix)
        candidates: List of objects to search
        key: Optional function to extract name from candidate (default: obj.name)

    Returns:
        Matching candidate or None if no match found
    """
    if not candidates:
        return None

    ordinal, search_name = parse_target(name)
    search_lower = search_name.lower()

    # Helper to get name from candidate
    def get_name(obj: Any) -> str:
        if key:
            return key(obj)
        return obj.name

    # Helper to get aliases from candidate
    def get_aliases(obj: Any) -> list[str]:
        if hasattr(obj, "aliases"):
            return obj.aliases
        return []

    # Collect all matches in priority order
    matches: list[Any] = []

    # Priority 1: Exact name match
    for candidate in candidates:
        if get_name(candidate).lower() == search_lower:
            matches.append(candidate)

    # Priority 2: Prefix name match
    if not matches:
        for candidate in candidates:
            if get_name(candidate).lower().startswith(search_lower):
                matches.append(candidate)

    # Priority 3: Exact alias match
    if not matches:
        for candidate in candidates:
            for alias in get_aliases(candidate):
                if alias.lower() == search_lower:
                    matches.append(candidate)
                    break

    # Priority 4: Prefix alias match
    if not matches:
        for candidate in candidates:
            for alias in get_aliases(candidate):
                if alias.lower().startswith(search_lower):
                    matches.append(candidate)
                    break

    # Return Nth match based on ordinal
    if len(matches) >= ordinal:
        return matches[ordinal - 1]

    return None


def find_entity_on_tile(
    name: str, player: "Entity", *, z_filter: bool = True
) -> "Entity | None":
    """Find a player or mob on the same tile as the player.

    By default, only finds entities on the same z-axis (both flying or both grounded).
    Skips the player themselves and dead mobs.

    Args:
        name: Target name (may include ordinal prefix)
        player: The player doing the searching
        z_filter: If True, filter by z-axis match (default True)

    Returns:
        Matching Entity or None
    """
    if not player.location or not hasattr(player.location, "contents_at"):
        return None

    # Get all entities at the player's position
    contents = player.location.contents_at(player.x, player.y)  # type: ignore[misc]

    # Filter to Entity instances, excluding self and dead mobs
    candidates = []
    for obj in contents:
        if not isinstance(obj, Entity):
            continue
        if obj is player:
            continue
        if hasattr(obj, "alive") and not obj.alive:
            continue
        # Check z-axis match if filtering enabled
        if z_filter and (
            getattr(obj, "flying", False) != getattr(player, "flying", False)
        ):
            continue
        candidates.append(obj)

    # Sort to prefer Players over Mobs (Players have reader/writer attributes)
    candidates.sort(key=lambda e: not hasattr(e, "reader"))

    return resolve_target(name, candidates)


def find_thing_on_tile(name: str, zone: "Zone", x: int, y: int) -> "Thing | None":
    """Find a Thing on the ground at the given coordinates.

    Args:
        name: Target name (may include ordinal prefix)
        zone: The zone to search in
        x: X coordinate
        y: Y coordinate

    Returns:
        Matching Thing or None
    """
    contents = zone.contents_at(x, y)

    # Filter to Thing instances only
    candidates = [obj for obj in contents if isinstance(obj, Thing)]

    return resolve_target(name, candidates)


def find_in_inventory(name: str, player: "Entity") -> "Thing | None":
    """Find a Thing in the player's inventory.

    Args:
        name: Target name (may include ordinal prefix)
        player: The player whose inventory to search

    Returns:
        Matching Thing or None
    """
    # Filter to Thing instances in inventory
    candidates = [obj for obj in player.contents if isinstance(obj, Thing)]

    return resolve_target(name, candidates)
