"""Utilities for resolving tools and partials.

A well-defined object resolver is a callable that takes a name and returns
an object.

## Key Operations

| Function              | Description                                                |
|-----------------------|------------------------------------------------------------|
| `resolve`             | Core function to resolve a named object using a resolver.  |
| `resolve_tool`        | Helper for resolving tool names.                           |
| `resolve_partial`     | Helper for resolving partial names.                        |
| `resolve_json_schema` | Helper for resolving JSON schemas.                         |

The `resolve_*` functions are convenience wrappers around `resolve` that handle
the specific types of resolvers for tools, partials, and schemas.

## Exceptions

| Exception              | Raised When                                        |
|------------------------|----------------------------------------------------|
| `ResolverFailedError`  | A resolver function raises an exception while      |
|                        | attempting to resolve a tool, schema, or partial   |
"""

from collections.abc import Callable
from typing import Any

from dotpromptz.typing import (
    JsonSchema,
    PartialResolver,
    SchemaResolver,
    ToolDefinition,
    ToolResolver,
)

# Resolver callable type.
ResolverCallable = Callable[[str], Any]


class ResolverFailedError(RuntimeError):
    """Raised when a resolver function fails to resolve an object.

    This exception wraps errors that occur during the resolution of tools,
    schemas, or partials. It preserves the original error context while
    providing structured access to error details.

    Attributes:
        name: The name of the object that failed to resolve (e.g., tool name).
        kind: The category of object ('tool', 'schema', or 'partial').
        reason: A human-readable description of why resolution failed.
    """

    def __init__(self, name: str, kind: str, reason: str) -> None:
        """Initialize the error with resolution context.

        Args:
            name: The name of the object that failed to resolve.
            kind: The kind of object that failed to resolve
                  ('tool', 'schema', or 'partial').
            reason: The reason the object resolver failed.
        """
        self.name = name
        self.kind = kind
        self.reason = reason
        super().__init__(f'{kind} resolver failed for {name}; {reason}')

    def __str__(self) -> str:
        """Return a human-readable error message.

        Returns:
            A formatted string describing the resolution failure.
        """
        return f'{self.kind} resolver failed for {self.name}; {self.reason}'

    def __repr__(self) -> str:
        """Return a detailed string representation for debugging.

        Returns:
            A formatted string with full error details.
        """
        return f'ResolverFailedError(name={self.name!r}, kind={self.kind!r}, reason={self.reason!r})'


def resolve[ResolverT: ResolverCallable, DefinitionT](
    name: str,
    kind: str,
    resolver: ResolverT | None,
) -> DefinitionT:
    """Resolves a single object using the provided resolver.

    Args:
        name: The name of the object to resolve.
        kind: The kind of object to resolve.
        resolver: The object resolver callable.

    Returns:
        The resolved object.

    Raises:
        LookupError: If the resolver returns None for the object.
        ResolverFailedError: For exceptions raised by the resolver.
        TypeError: If the resolver is not callable or returns an invalid type.
        ValueError: If the resolver is not defined.
    """
    obj: DefinitionT | None = None

    if resolver is None:
        raise ValueError(f'{kind} resolver is not defined')

    if not callable(resolver):
        raise TypeError(f"{kind} resolver for '{name}' is not callable")

    try:
        obj = resolver(name)
    except Exception as e:
        raise ResolverFailedError(name, kind, str(e)) from e

    # Raise when the resolver indicates the object doesn't exist.
    if obj is None:
        raise LookupError(f"{kind} resolver for '{name}' returned None")

    return obj


def resolve_tool(name: str, resolver: ToolResolver | None) -> ToolDefinition:
    """Resolve a tool using the provided resolver.

    Args:
        name: The name of the tool to resolve.
        resolver: The tool resolver callable.

    Returns:
        The resolved tool definition.

    Raises:
        LookupError: If the resolver returns None for the tool.
        ResolverFailedError: For exceptions raised by the resolver.
        TypeError: If the resolver is not callable or returns an invalid type.
        ValueError: If the resolver is not defined.
    """
    return resolve(name, 'tool', resolver)


def resolve_partial(name: str, resolver: PartialResolver | None) -> str:
    """Resolve a partial using the provided resolver.

    Args:
        name: The name of the partial to resolve.
        resolver: The partial resolver callable.

    Returns:
        The resolved partial.

    Raises:
        LookupError: If the resolver returns None for the partial.
        ResolverFailedError: For exceptions raised by the resolver.
        TypeError: If the resolver is not callable or returns an invalid type.
        ValueError: If the resolver is not defined.
    """
    return resolve(name, 'partial', resolver)


def resolve_json_schema(name: str, resolver: SchemaResolver | None) -> JsonSchema:
    """Resolve a JSON schema using the provided resolver.

    Args:
        name: The name of the JSON schema to resolve.
        resolver: The JSON schema resolver callable.

    Returns:
        The resolved JSON schema.

    Raises:
        LookupError: If the resolver returns None for the schema.
        ResolverFailedError: For exceptions raised by the resolver.
        TypeError: If the resolver is not callable or returns an invalid type.
    """
    return resolve(name, 'schema', resolver)
