"""Resource type inheritance resolution engine.

Implements ADR-042: single-inheritance resource type specialization with
field resolution, collection merging, chain validation, and polymorphic
type checking.

Key functions:

| Function                     | Purpose                                       |
|------------------------------|-----------------------------------------------|
| ``resolve_inheritance_chain``| Walk ``inherits`` links from child to root     |
| ``resolve_fields``           | Merge fields along the chain (subtype wins)    |
| ``is_subtype_of``            | Polymorphic check: does A inherit from B?      |
| ``validate_chain``           | Registration-time chain validation             |

Chain rules (ADR-042 §Inheritance Chain Rules):

1. Single inheritance only (at most one ``inherits`` value).
2. Maximum chain depth of 5 levels.
3. No circular inheritance.
4. Built-in types may not inherit from custom (namespaced) types.
5. Removing a parent type is prohibited while subtypes exist.

See Also:
    - ``docs/adr/ADR-042-resource-type-inheritance.md``
    - ``docs/specification.md`` lines 23460-23479
"""

from __future__ import annotations

from typing import Any

import structlog

logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)

#: Type alias for registry entries — each value is a dict or object with
#: an ``inherits`` key/attribute.  Internal helpers also read ``handler``,
#: ``cli_args``, ``child_types``, ``parent_types``, and ``properties``.
TypeRegistryMap = dict[str, Any]

__all__ = [
    "MAX_CHAIN_DEPTH",
    "ResourceTypeCircularInheritanceError",
    "ResourceTypeInheritanceDepthError",
    "ResourceTypeParentNotFoundError",
    "ResourceTypeParentRemovalError",
    "TypeRegistryMap",
    "find_subtypes",
    "is_subtype_of",
    "resolve_fields",
    "resolve_inheritance_chain",
    "validate_chain",
]

#: Maximum inheritance chain depth (ADR-042 rule 2).
MAX_CHAIN_DEPTH: int = 5


class ResourceTypeInheritanceDepthError(ValueError):
    """Raised when an inheritance chain exceeds the maximum depth."""


class ResourceTypeCircularInheritanceError(ValueError):
    """Raised when a circular inheritance chain is detected."""


class ResourceTypeParentNotFoundError(ValueError):
    """Raised when a parent type referenced by ``inherits`` is not registered."""


class ResourceTypeParentRemovalError(ValueError):
    """Raised when attempting to remove a type that has registered subtypes."""


def resolve_inheritance_chain(
    type_name: str,
    type_registry: TypeRegistryMap,
) -> list[str]:
    """Walk the ``inherits`` links from *type_name* to the root type.

    Returns the full chain as a list starting with *type_name* and
    ending with the root ancestor (the first type that has no
    ``inherits`` value).

    Args:
        type_name: Name of the type to resolve.
        type_registry: Mapping of type name to type definition.  Each
            definition must support ``inherits`` as a key (dict) or
            attribute (object with ``.inherits``).

    Returns:
        List of type names from child to root, e.g.
        ``["devcontainer-instance", "container-instance"]``.

    Raises:
        ResourceTypeParentNotFoundError: If *type_name* or any parent
            in the chain is not in *type_registry*.
        ResourceTypeCircularInheritanceError: If a cycle is detected.
        ResourceTypeInheritanceDepthError: If the chain exceeds
            :data:`MAX_CHAIN_DEPTH` levels.

    .. note::

        If *type_name* itself is not in *type_registry* and
        ``allow_missing_root`` semantics are needed, callers (e.g.
        ``is_subtype_of``) should catch
        ``ResourceTypeParentNotFoundError``.
    """
    chain: list[str] = []
    visited: set[str] = set()
    current = type_name

    while current is not None:
        if current in visited:
            raise ResourceTypeCircularInheritanceError(
                f"Circular inheritance detected: {' -> '.join(chain)} -> {current}"
            )

        if len(chain) >= MAX_CHAIN_DEPTH:
            raise ResourceTypeInheritanceDepthError(
                f"Inheritance chain for '{type_name}' exceeds the maximum "
                f"depth of {MAX_CHAIN_DEPTH}: {' -> '.join(chain)}"
            )

        visited.add(current)
        chain.append(current)

        entry = type_registry.get(current)
        if entry is None:
            if current != type_name:
                raise ResourceTypeParentNotFoundError(
                    f"Parent type '{current}' (in chain for '{type_name}') "
                    "is not registered."
                )
            # type_name itself is not in the registry.  Log a warning
            # and return [type_name] so that callers like is_subtype_of()
            # can treat unregistered types as root types with no ancestors
            # (returns False for any ancestor check).  Service-level
            # callers should verify existence before calling.
            logger.warning(
                "resolve_inheritance_chain called for unregistered type",
                type_name=type_name,
            )
            break

        parent = _get_inherits(entry)
        current = parent

    return chain


def validate_chain(
    type_name: str,
    inherits: str | None,
    type_registry: TypeRegistryMap,
    *,
    is_built_in: bool = False,
) -> list[str]:
    """Validate an inheritance declaration at registration time.

    Checks all five ADR-042 chain rules and returns the resolved chain
    on success.

    Args:
        type_name: Name of the type being registered.
        inherits: Parent type name (or ``None`` for root types).
        type_registry: Current registry contents (must not yet contain
            *type_name*).
        is_built_in: Whether the type being registered is built-in.

    Returns:
        The resolved inheritance chain (child to root).

    Raises:
        ResourceTypeParentNotFoundError: If *inherits* names a type
            not in *type_registry*.
        ResourceTypeCircularInheritanceError: If adding this type
            would create a cycle.
        ResourceTypeInheritanceDepthError: If the resulting chain
            would exceed :data:`MAX_CHAIN_DEPTH`.
        ValueError: If a built-in type tries to inherit from a custom
            type, or if *inherits* is whitespace-only.
    """
    if inherits is None:
        return [type_name]

    # Reject whitespace-only inherits values.
    if not inherits.strip():
        raise ValueError(
            f"'inherits' for '{type_name}' must not be empty or whitespace-only."
        )

    # Rule 1: single inheritance is enforced by the schema (one field).

    # Rule 4: built-in types must not inherit from custom types.
    if is_built_in and "/" in inherits:
        raise ValueError(
            f"Built-in type '{type_name}' cannot inherit from custom type '{inherits}'."
        )

    # Parent must exist.
    if inherits not in type_registry:
        raise ResourceTypeParentNotFoundError(
            f"Parent type '{inherits}' is not registered. "
            f"Cannot register '{type_name}' with inherits='{inherits}'."
        )

    # Build a temporary registry entry to check the chain.
    temp_registry = dict(type_registry)
    temp_registry[type_name] = {"inherits": inherits}

    # This will enforce rules 2 (depth) and 3 (cycles).
    return resolve_inheritance_chain(type_name, temp_registry)


def is_subtype_of(
    type_name: str,
    ancestor_name: str,
    type_registry: TypeRegistryMap,
) -> bool:
    """Check whether *type_name* is the same as or a subtype of *ancestor_name*.

    This is the core polymorphism check used by:
    - Tool binding (ADR-042 §Tool Binding Polymorphism)
    - Auto-discovery child type matching
    - DAG query filtering

    Args:
        type_name: The concrete type to check.
        ancestor_name: The declared/expected type.
        type_registry: Current registry contents.

    Returns:
        ``True`` if *type_name* equals *ancestor_name* or transitively
        inherits from it.
    """
    if type_name == ancestor_name:
        return True

    try:
        chain = resolve_inheritance_chain(type_name, type_registry)
    except (
        ResourceTypeParentNotFoundError,
        ResourceTypeCircularInheritanceError,
        ResourceTypeInheritanceDepthError,
    ):
        return False

    return ancestor_name in chain


def resolve_fields(
    type_name: str,
    type_registry: TypeRegistryMap,
) -> dict[str, Any]:
    """Resolve the merged fields for *type_name* along its inheritance chain.

    Field resolution (ADR-042 §Field Resolution):
    - Scalar fields: subtype's value overrides parent's.
    - Collection fields (``cli_args``, ``child_types``, ``parent_types``,
      ``properties``): additive merging by default;
      ``<field>_replace: true`` replaces.

    Args:
        type_name: Name of the type to resolve.
        type_registry: Current registry contents.

    Returns:
        A **defensive copy** of the fully resolved field values.
        Callers may mutate the returned dict without affecting the
        registry.
    """
    chain = resolve_inheritance_chain(type_name, type_registry)

    # Walk from root to child (so child overrides parent).
    merged: dict[str, Any] = {}
    for name in reversed(chain):
        entry = type_registry.get(name)
        if entry is None:
            continue

        fields = _to_dict(entry)

        # Scalar fields: subtype wins.
        for key, value in fields.items():
            if key in _COLLECTION_FIELDS:
                continue
            if key.endswith("_replace"):
                continue
            if key == "inherits":
                continue
            merged[key] = value

        # Collection fields: additive merge or replace.
        for field_name in _COLLECTION_FIELDS:
            replace_key = f"{field_name}_replace"
            should_replace = fields.get(replace_key, False)
            child_value = fields.get(field_name)

            if child_value is None:
                continue

            if should_replace or field_name not in merged:
                merged[field_name] = child_value
            else:
                merged[field_name] = _merge_collection(
                    field_name,
                    merged[field_name],
                    child_value,
                )

    # Preserve the inherits field from the original type.
    original = type_registry.get(type_name)
    if original is not None:
        merged["inherits"] = _get_inherits(original)

    # Return a defensive copy so callers cannot corrupt the registry.
    return dict(merged)


def find_subtypes(
    ancestor_name: str,
    type_registry: TypeRegistryMap,
) -> list[str]:
    """Find all types that inherit from *ancestor_name* (directly or transitively).

    .. note::

        The returned list does **not** include *ancestor_name* itself,
        only its descendants.

    Args:
        ancestor_name: The ancestor type name.
        type_registry: Current registry contents.

    Returns:
        List of subtype names (excludes *ancestor_name* itself).
    """
    subtypes: list[str] = []
    for name in type_registry:
        if name == ancestor_name:
            continue
        if is_subtype_of(name, ancestor_name, type_registry):
            subtypes.append(name)
    return subtypes


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

#: Collection fields that use additive merging (ADR-042 §Collection Field Merging).
#: Note: ``properties`` is intentionally excluded.  ``ResourceTypeSpec`` has no
#: ``properties`` field — property inheritance only applies to raw-dict registry
#: entries used in tests.  Domain-model merging is authoritative.
_COLLECTION_FIELDS: frozenset[str] = frozenset(
    {"cli_args", "child_types", "parent_types"}
)


def _get_inherits(entry: Any) -> str | None:
    """Extract the ``inherits`` value from a registry entry (dict or object)."""
    if isinstance(entry, dict):
        return entry.get("inherits")
    return getattr(entry, "inherits", None)


def _to_dict(entry: Any) -> dict[str, Any]:
    """Convert a registry entry to a dict for field merging."""
    if isinstance(entry, dict):
        return dict(entry)
    if hasattr(entry, "model_dump"):
        return entry.model_dump()
    return {k: v for k, v in vars(entry).items() if not k.startswith("_")}


def _merge_collection(
    field_name: str,
    parent_value: Any,
    child_value: Any,
) -> Any:
    """Merge a collection field using additive semantics.

    For ``cli_args``: child entries with the same ``name`` replace parent
    entries (same-name replacement).  For ``child_types`` and
    ``parent_types``: union of both lists (deduplication).  For
    ``properties``: dict merge where child keys override parent keys.
    """
    if field_name == "cli_args":
        return _merge_cli_args(parent_value, child_value)
    if field_name == "properties":
        return _merge_dict(parent_value, child_value)
    # child_types and parent_types: union, preserving order, child last.
    return _merge_string_list(parent_value, child_value)


def _merge_cli_args(
    parent_args: list[Any],
    child_args: list[Any],
) -> list[Any]:
    """Merge CLI argument lists with same-name replacement.

    Child args with the same ``name`` as a parent arg replace the parent
    entry.  Otherwise child args are appended.
    """
    if not parent_args:
        return list(child_args)
    if not child_args:
        return list(parent_args)

    # Build lookup of child arg names for O(1) checking.
    child_names: set[str] = set()
    for arg in child_args:
        name = arg.get("name") if isinstance(arg, dict) else getattr(arg, "name", None)
        if name:
            child_names.add(name)

    # Keep parent args that are NOT overridden by child.
    merged: list[Any] = []
    for arg in parent_args:
        name = arg.get("name") if isinstance(arg, dict) else getattr(arg, "name", None)
        if name not in child_names:
            merged.append(arg)

    # Append all child args.
    merged.extend(child_args)
    return merged


def _merge_dict(
    parent: dict[str, Any],
    child: dict[str, Any],
) -> dict[str, Any]:
    """Merge two dicts with child keys overriding parent keys.

    Used for ``properties`` field merging where parent provides
    defaults and child can override individual keys.
    """
    merged = dict(parent) if parent else {}
    if child:
        merged.update(child)
    return merged


def _merge_string_list(
    parent: list[str],
    child: list[str],
) -> list[str]:
    """Merge two string lists with deduplication, preserving order."""
    seen: set[str] = set()
    merged: list[str] = []
    for item in parent:
        if item not in seen:
            seen.add(item)
            merged.append(item)
    for item in child:
        if item not in seen:
            seen.add(item)
            merged.append(item)
    return merged
