"""Built-in component templates for the ``agentmux new`` scaffolding command.

Each template produces a valid canonical-format file (Markdown + YAML frontmatter,
or JSON for MCP configs) that can be parsed by the corresponding ``from_file()`` /
``from_dir()`` methods on the component models.

``TEMPLATES`` maps a :class:`~agentmux.core.component.ComponentType` to a
:data:`ComponentTemplate` that describes how to render and where to write the
scaffold.

Usage::

    template = TEMPLATES[ComponentType.AGENT]
    content = template.render(name="my-agent", description="Does things")
    # Write content to template.output_filename("my-agent")
"""

import json
import string
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from agentmux.core.component import ComponentType
from agentmux.utils.frontmatter import dump as dump_frontmatter

__all__ = ["ComponentTemplate", "TEMPLATES", "render_template"]

# ---------------------------------------------------------------------------
# Body-only template strings (no frontmatter — that is emitted programmatically)
# ---------------------------------------------------------------------------

_AGENT_BODY = """\

# $name

Describe what this agent does and how it should behave.
"""

_COMMAND_BODY = """\

# $name

Describe what this command does.

## Usage

Explain how to use this command.
"""

_SKILL_BODY = """\

# $name

Describe what this skill provides and how AI tools should use it.

## Instructions

Add your skill instructions here.
"""

_RULE_BODY = """\

# $name

Describe the rule or convention this file enforces.
"""

_INSTRUCTIONS_BODY = """\
# Instructions

Add your global AI tool instructions here.
"""

# MCP uses a JSON template — rendered separately.
_MCP_TEMPLATE_DATA: dict[str, Any] = {
    "mcpServers": {},
}


# ---------------------------------------------------------------------------
# Frontmatter builders — return the metadata dict for each component type
# ---------------------------------------------------------------------------

FrontmatterBuilder = Callable[[str, str], dict[str, Any]]


def _standard_meta(name: str, description: str) -> dict[str, Any]:
    meta: dict[str, Any] = {"name": name}
    if description:
        meta["description"] = description
    return meta


def _rule_meta(name: str, description: str) -> dict[str, Any]:
    meta: dict[str, Any] = {"name": name, "alwaysApply": False}
    if description:
        meta["description"] = description
    return meta


def _no_meta(_name: str, _description: str) -> dict[str, Any]:
    """Used for component types with no structured frontmatter (Instructions)."""
    return {}


# ---------------------------------------------------------------------------
# ComponentTemplate dataclass
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class ComponentTemplate:
    """Describes how to scaffold a single component type.

    Attributes:
        component_type: The :class:`ComponentType` this template belongs to.
        body_raw: A ``string.Template``-compatible body string (no frontmatter).
            ``None`` for component types that are not plain Markdown
            (e.g. MCP JSON).
        meta_builder: A callable ``(name, description) -> dict`` that produces
            the YAML frontmatter metadata for the rendered file.  ``None`` for
            types with no frontmatter.
        default_subdir: Conventional subdirectory under the project root where
            the new file should be created when no ``agentmux.toml`` is present.
        filename_pattern: A ``string.Template`` that produces the output
            *filename* (not the full path) given ``name``.
        creates_directory: When ``True``, the scaffold creates a *directory*
            containing the rendered file (used for skills).
    """

    component_type: ComponentType
    body_raw: str | None
    meta_builder: FrontmatterBuilder | None
    default_subdir: str
    filename_pattern: str
    creates_directory: bool = False

    def render(self, name: str, description: str = "") -> str:
        """Render the template with the given ``name`` and ``description``.

        The frontmatter is built programmatically via :attr:`meta_builder` so
        that keys with empty values are omitted — producing clean YAML that
        round-trips through the component models without validation errors.

        Args:
            name: The component name.
            description: An optional short description.

        Returns:
            The rendered file content as a string.

        Raises:
            NotImplementedError: If the template type does not support text
                rendering (e.g. MCP — use :func:`render_mcp` instead).
        """
        if self.body_raw is None or self.meta_builder is None:
            raise NotImplementedError(
                f"Template for {self.component_type.value!r} does not support text rendering. "
                "Use render_mcp() for MCP configs."
            )
        body = string.Template(self.body_raw).substitute(name=name)
        meta = self.meta_builder(name, description)
        return dump_frontmatter(meta, body)

    def output_filename(self, name: str) -> str:
        """Return the output filename for the given component name.

        Args:
            name: The component name (used for ``$name`` substitutions).
        """
        return string.Template(self.filename_pattern).substitute(name=name)


def render_mcp() -> str:
    """Render the MCP config template as a JSON string.

    Returns:
        A pretty-printed JSON string with an empty ``mcpServers`` object.
    """
    return json.dumps(_MCP_TEMPLATE_DATA, indent=2)


# ---------------------------------------------------------------------------
# TEMPLATES registry
# ---------------------------------------------------------------------------

TEMPLATES: dict[ComponentType, ComponentTemplate] = {
    ComponentType.AGENT: ComponentTemplate(
        component_type=ComponentType.AGENT,
        body_raw=_AGENT_BODY,
        meta_builder=_standard_meta,
        default_subdir="agents",
        filename_pattern="$name.md",
    ),
    ComponentType.COMMAND: ComponentTemplate(
        component_type=ComponentType.COMMAND,
        body_raw=_COMMAND_BODY,
        meta_builder=_standard_meta,
        default_subdir="commands",
        filename_pattern="$name.md",
    ),
    ComponentType.SKILL: ComponentTemplate(
        component_type=ComponentType.SKILL,
        body_raw=_SKILL_BODY,
        meta_builder=_standard_meta,
        default_subdir="skills",
        filename_pattern="$name",
        creates_directory=True,
    ),
    ComponentType.RULE: ComponentTemplate(
        component_type=ComponentType.RULE,
        body_raw=_RULE_BODY,
        meta_builder=_rule_meta,
        default_subdir="rules",
        filename_pattern="$name.md",
    ),
    ComponentType.INSTRUCTIONS: ComponentTemplate(
        component_type=ComponentType.INSTRUCTIONS,
        body_raw=_INSTRUCTIONS_BODY,
        meta_builder=_no_meta,
        default_subdir=".",
        filename_pattern="instructions.md",
    ),
    ComponentType.MCP: ComponentTemplate(
        component_type=ComponentType.MCP,
        body_raw=None,
        meta_builder=None,
        default_subdir=".",
        # MCP config is always named "mcp.json" regardless of component name —
        # the $name substitution in output_filename() is a no-op here.
        filename_pattern="mcp.json",
    ),
}


def render_template(component_type: ComponentType, name: str, description: str = "") -> str:
    """Render the template for a given component type.

    Convenience wrapper that dispatches to either
    :meth:`ComponentTemplate.render` or :func:`render_mcp`.

    Args:
        component_type: The type of component to scaffold.
        name: The component name.
        description: An optional short description.

    Returns:
        The rendered file content as a string.
    """
    template = TEMPLATES[component_type]
    if component_type == ComponentType.MCP:
        return render_mcp()
    return template.render(name=name, description=description)


# Ensure every ComponentType has an entry so callers can rely on the mapping
# being exhaustive.  This is checked at import time.
_missing = [ct for ct in ComponentType if ct not in TEMPLATES]
if _missing:  # pragma: no cover
    raise RuntimeError(
        f"TEMPLATES is missing entries for: {[ct.value for ct in _missing]}. "
        "Update agentmux.core.templates to add the missing component types."
    )
