"""Asset-backed node groups.

An :class:`AssetNodeGroup` behaves like a :class:`NodeGroupBuilder`, but instead
of *building* its inner tree it **appends** a named node group from a ``.blend``
asset library and points a Group node at it. Subclasses (typically generated by
:func:`nodebpy.assets.generate_asset_api`) declare the library + asset name and a
typed interface, so an asset reads and type-checks like any other node.
"""

from __future__ import annotations

import os
from abc import ABC, abstractmethod
from pathlib import Path

import bpy

from .node import (
    CustomCompositorGroup,
    CustomGeometryGroup,
    CustomShaderGroup,
)


class AssetLibrary(ABC):
    """Locates a ``.blend`` asset library on disk at runtime."""

    @abstractmethod
    def path(self) -> str:
        """Absolute path to the ``.blend`` file."""
        ...


class BundledLibrary(AssetLibrary):
    """A node-group asset library shipped with Blender itself, under the system
    datafiles (``…/datafiles/assets/nodes/<filename>``) — e.g. the
    ``geometry_nodes_essentials.blend`` essentials library."""

    def __init__(self, filename: str):
        self.filename = filename

    def path(self) -> str:
        nodes_dir = os.path.join(
            bpy.utils.system_resource("DATAFILES"),  # ty: ignore[unresolved-attribute]
            "assets",
            "nodes",
        )
        return os.path.join(nodes_dir, self.filename)


class PackageLibrary(AssetLibrary):
    """A ``.blend`` shipped inside a Python package, located relative to a module
    file. Pass ``anchor=__file__`` from the generated module and the path to the
    ``.blend`` relative to it::

        PackageLibrary(__file__, "../data/my_assets.blend")
    """

    def __init__(self, anchor: str, relative: str):
        self.anchor = anchor
        self.relative = relative

    def path(self) -> str:
        return str((Path(self.anchor).parent / self.relative).resolve())


class _AssetGroupMixin:
    """Mixin that turns a ``Custom*Group`` into an asset-backed group: it appends
    ``_asset_name`` from ``_library`` (reusing an already-appended group of that
    name) rather than building the tree."""

    _library: AssetLibrary
    _asset_name: str
    _tree_idname: str  # provided by the Custom*Group this is mixed into

    def _build_group(self, tree) -> None:  # pragma: no cover - never called
        raise NotImplementedError(
            "Asset groups are appended from a library, not built."
        )

    @classmethod
    def create_group(cls):
        """Append the named asset group from the library and return its tree,
        reusing a previously-appended group of the same name and tree type.

        The tree-type check matters because asset names collide across editors
        (a geometry and a compositor "Combine Spherical" both exist); reusing by
        name alone could hand back a group of the wrong type.
        """
        existing = bpy.data.node_groups.get(cls._asset_name)
        if existing is not None and existing.bl_idname == cls._tree_idname:
            return existing
        path = cls._library.path()
        if not os.path.exists(path):
            raise FileNotFoundError(f"Asset library not found: {path}")
        with bpy.data.libraries.load(path, link=True, pack=True, assets_only=True) as (  # ty: ignore[invalid-context-manager]
            src,
            dst,
        ):
            if cls._asset_name not in src.node_groups:
                raise KeyError(f"Node group {cls._asset_name!r} not found in {path}")
            dst.node_groups = [cls._asset_name]
        return dst.node_groups[0]


class AssetGeometryGroup(_AssetGroupMixin, CustomGeometryGroup):
    """A Geometry Nodes group appended from an asset library."""


class AssetShaderGroup(_AssetGroupMixin, CustomShaderGroup):
    """A Shader node group appended from an asset library."""


class AssetCompositorGroup(_AssetGroupMixin, CustomCompositorGroup):
    """A Compositor node group appended from an asset library."""


_ASSET_GROUP_BASE = {
    "GeometryNodeTree": AssetGeometryGroup,
    "ShaderNodeTree": AssetShaderGroup,
    "CompositorNodeTree": AssetCompositorGroup,
}


def asset_group_base(tree_idname: str) -> type:
    """The ``Asset*Group`` base class for a tree-type bl_idname."""
    return _ASSET_GROUP_BASE[tree_idname]


__all__ = [
    "AssetLibrary",
    "BundledLibrary",
    "PackageLibrary",
    "AssetNodeGroup",
    "AssetGeometryGroup",
    "AssetShaderGroup",
    "AssetCompositorGroup",
    "asset_group_base",
]

# Public alias for the mixin, for type hints / isinstance in generated code.
AssetNodeGroup = _AssetGroupMixin
