"""Plugin-contributed Automation Blueprints (``ctx.register_automation_blueprint``).

A plugin hands over the same fields an in-repo :class:`~cron.blueprint_catalog.AutomationBlueprint`
carries; :func:`build_plugin_blueprint` validates them (bad input raises ``ValueError``, which the
registrar logs and ignores) and namespaces the key as ``<plugin>:<key>``, so a plugin can never
shadow a built-in. :func:`plugin_blueprints` reads the ACTIVE profile's plugin manager at call time;
plugins load per profile, so nothing here is cached across profiles.
"""

from __future__ import annotations

import logging
import re
import string
from dataclasses import fields
from typing import Any, Iterable, List, Mapping

from cron.blueprint_catalog import AutomationBlueprint, BlueprintSlot, fill_blueprint

logger = logging.getLogger(__name__)

__all__ = ["build_plugin_blueprint", "plugin_blueprints"]

_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
_NAMESPACE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$")
_SLOT_NAME_RE = re.compile(r"^[A-Za-z_]\w{0,63}$")
_SLOT_FIELDS = frozenset(f.name for f in fields(BlueprintSlot))
# Placeholders ``_resolve_schedule`` derives itself: minute/hour from the ``time`` slot, dow from a
# ``recurrence``/``day`` slot (or ``*`` when neither exists).
_DERIVED_SCHEDULE_FIELDS = {"minute": "time", "hour": "time", "dow": None}


def _text(name: str, value: Any) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{name} must be a non-empty string")
    return value.strip()


def _strings(name: str, value: Any) -> tuple:
    if isinstance(value, str) or not isinstance(value, Iterable):
        raise ValueError(f"{name} must be a list of strings")
    items = tuple(value)
    if not all(isinstance(v, str) and v.strip() for v in items):
        raise ValueError(f"{name} must be a list of non-empty strings")
    return items


def _slot(raw: Any) -> BlueprintSlot:
    """Accept a ``BlueprintSlot`` (in-process) or its field mapping (the plugin-host wire form)."""
    if isinstance(raw, BlueprintSlot):
        slot = raw
    elif isinstance(raw, Mapping):
        unknown = sorted(set(raw) - _SLOT_FIELDS)
        if unknown:
            raise ValueError(f"unknown slot field(s): {', '.join(unknown)}")
        data = dict(raw)
        if "options" in data:
            data["options"] = tuple(data["options"] or ())
        try:
            slot = BlueprintSlot(**data)
        except TypeError as exc:  # missing name/type/label
            raise ValueError(f"slot {data.get('name')!r}: {exc}") from exc
    else:
        raise ValueError(f"slot must be a mapping, got {type(raw).__name__}")
    if not isinstance(slot.name, str) or not _SLOT_NAME_RE.match(slot.name):
        raise ValueError(f"invalid slot name {slot.name!r}")
    _text(f"slot {slot.name} label", slot.label)
    return slot


def _placeholders(template: str, what: str) -> list[str]:
    names = []
    for _literal, name, _spec, _conv in string.Formatter().parse(template):
        if name is None:
            continue
        if not _SLOT_NAME_RE.match(name):
            raise ValueError(f"{what} placeholder {{{name}}} must be a plain slot name")
        names.append(name)
    return names


def _validate(blueprint: AutomationBlueprint) -> None:
    by_name = {s.name: s for s in blueprint.slots}
    if len(by_name) != len(blueprint.slots):
        raise ValueError("slot names must be unique")
    for name in _placeholders(blueprint.prompt_template, "prompt_template"):
        slot = by_name.get(name)
        if slot is None:
            raise ValueError(f"prompt_template uses {{{name}}} but there is no slot named {name!r}")
        if slot.optional and slot.default in (None, ""):
            raise ValueError(f"prompt_template uses optional slot {name!r} that has no default")
    for name in _placeholders(blueprint.schedule_template, "schedule_template"):
        if name in _DERIVED_SCHEDULE_FIELDS:
            needed = _DERIVED_SCHEDULE_FIELDS[name]
            if needed and needed not in by_name:
                raise ValueError(f"schedule_template uses {{{name}}}, which needs a slot named {needed!r}")
        elif name not in by_name:
            raise ValueError(f"schedule_template uses {{{name}}} but there is no slot named {name!r}")
    # When every required slot has a default, fill it now so a broken template or schedule fails at
    # registration instead of on the user's first "Schedule it".
    if all(s.optional or s.default not in (None, "") for s in blueprint.slots):
        from cron.jobs import parse_schedule

        parse_schedule(fill_blueprint(blueprint, {})["schedule"])


def build_plugin_blueprint(
    plugin: str, key: str, *, title: str, description: str, schedule_template: str,
    prompt_template: str, category: str = "general", slots: Iterable[Any] = (),
    deliver_default: str = "origin", skills: Iterable[str] = (), tags: Iterable[str] = (),
) -> AutomationBlueprint:
    """Validated blueprint keyed ``<plugin>:<key>``; raises ``ValueError`` on malformed input."""
    if not isinstance(plugin, str) or not _NAMESPACE_RE.match(plugin):
        raise ValueError(f"plugin name {plugin!r} cannot namespace a blueprint key")
    if not isinstance(key, str) or not _KEY_RE.match(key):
        raise ValueError(
            f"invalid key {key!r}: use lowercase letters, digits, '-' or '_' (the "
            f"'{plugin}:' namespace is added automatically)"
        )
    if isinstance(slots, (str, Mapping)) or not isinstance(slots, Iterable):
        raise ValueError("slots must be a list")
    blueprint = AutomationBlueprint(
        key=f"{plugin}:{key}",
        title=_text("title", title),
        description=_text("description", description),
        category=_text("category", category),
        schedule_template=_text("schedule_template", schedule_template),
        prompt_template=_text("prompt_template", prompt_template),
        slots=[_slot(s) for s in slots],
        deliver_default=_text("deliver_default", deliver_default),
        skills=_strings("skills", skills),
        tags=_strings("tags", tags),
        plugin=plugin,
    )
    _validate(blueprint)
    return blueprint


def plugin_blueprints() -> list[AutomationBlueprint]:
    """Blueprints the active profile's enabled plugins registered (resolved per call)."""
    try:
        from hermes_cli.plugins import discover_plugins, get_plugin_manager

        discover_plugins()  # idempotent; servers that never import model_tools still see plugins
        return get_plugin_manager().list_automation_blueprints()
    except Exception:
        logger.warning("plugin automation blueprints unavailable", exc_info=True)
        return []
