# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

"""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.
"""

from collections.abc import Callable
from typing import Any

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

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


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)
