"""
Workflow generator service.

Thin facade over ``core.workflow.generator.WorkflowGenerator`` that owns the
model-manager / model-instance plumbing. Controllers call this; the pure
domain class never touches the model registry directly.

Pattern mirrors ``LLMGenerator.generate_rule_config`` — see
``core/llm_generator/llm_generator.py`` — but lives in ``services/`` because
the generator output is consumed at the application layer (sync_draft_workflow,
createApp) rather than from inside another workflow.
"""

import logging
from collections.abc import Iterator
from typing import Any

from core.app.app_config.entities import ModelConfig
from core.model_manager import ModelInstance, ModelManager
from core.workflow.generator import WorkflowGenerator
from core.workflow.generator.tool_catalogue import build_tool_catalogue, format_tool_catalogue, installed_tool_keys
from core.workflow.generator.types import (
    WorkflowGenerateResultDict,
    WorkflowGenerationModeRequest,
)
from graphon.model_runtime.entities.model_entities import ModelType

logger = logging.getLogger(__name__)


class WorkflowGeneratorService:
    """
    Coordinates model resolution with the workflow generator domain logic.

    Single public method (``generate_workflow_graph``) keeps the surface area
    minimal — the cmd+k `/create` flow is the only caller today.
    """

    @classmethod
    def generate_workflow_graph(
        cls,
        *,
        tenant_id: str,
        mode: WorkflowGenerationModeRequest,
        instruction: str,
        model_config: ModelConfig,
        ideal_output: str = "",
        current_graph: dict[str, Any] | None = None,
    ) -> WorkflowGenerateResultDict:
        """
        Resolve a model instance for the tenant and run the generator.

        ``mode`` accepts the ``"auto"`` sentinel — the planner itself picks the
        concrete ``workflow`` / ``advanced-chat`` mode (no extra LLM call) and
        the resolution is echoed back under the result's ``mode`` key.

        ``current_graph`` is the existing draft graph for the cmd+k `/refine`
        flow — when present the generator refines it instead of creating a new
        graph from scratch. ``None`` is the `/create` path.

        Errors from the LLM call (auth, quota, invoke) propagate so the
        controller can map them to existing HTTP error envelopes (same
        envelope as ``/rule-generate``).
        """
        model_instance, model_parameters, tool_catalogue_text, installed_tools = cls._resolve_generation_context(
            tenant_id=tenant_id, model_config=model_config
        )

        return WorkflowGenerator.generate_workflow_graph(
            model_instance=model_instance,
            model_parameters=model_parameters,
            provider=model_config.provider,
            model_name=model_config.name,
            model_mode=model_config.mode.value,
            mode=mode,
            instruction=instruction,
            ideal_output=ideal_output,
            tool_catalogue_text=tool_catalogue_text,
            installed_tools=installed_tools,
            current_graph=current_graph,
        )

    @classmethod
    def generate_workflow_graph_stream(
        cls,
        *,
        tenant_id: str,
        mode: WorkflowGenerationModeRequest,
        instruction: str,
        model_config: ModelConfig,
        ideal_output: str = "",
        current_graph: dict[str, Any] | None = None,
    ) -> Iterator[tuple[str, dict[str, Any]]]:
        """
        Streaming sibling of ``generate_workflow_graph``.

        Resolves the same model instance / tool catalogue, then delegates to
        ``WorkflowGenerator.generate_workflow_graph_stream`` and yields its
        ``(event_name, payload)`` tuples through to the controller's SSE
        writer. Provider-init / invoke errors raised while resolving the model
        instance propagate to the caller (the controller emits them as a
        single ``result`` SSE event).
        """
        model_instance, model_parameters, tool_catalogue_text, installed_tools = cls._resolve_generation_context(
            tenant_id=tenant_id, model_config=model_config
        )

        yield from WorkflowGenerator.generate_workflow_graph_stream(
            model_instance=model_instance,
            model_parameters=model_parameters,
            provider=model_config.provider,
            model_name=model_config.name,
            model_mode=model_config.mode.value,
            mode=mode,
            instruction=instruction,
            ideal_output=ideal_output,
            tool_catalogue_text=tool_catalogue_text,
            installed_tools=installed_tools,
            current_graph=current_graph,
        )

    @classmethod
    def _resolve_generation_context(
        cls,
        *,
        tenant_id: str,
        model_config: ModelConfig,
    ) -> tuple[ModelInstance, dict[str, Any], str, set[tuple[str, str]] | None]:
        """Resolve the model instance, completion params, and tool catalogue.

        Build the installed-tool catalogue for this tenant so the planner /
        builder can pick concrete tools instead of inventing names, AND so the
        runner's validator can reject hallucinated tool names BEFORE the user
        clicks Apply. A failure here (plugin daemon unreachable, etc.) must not
        block generation — log and fall back to the no-tool path, which also
        disables tool validation in the runner (``None`` sentinel rather than
        empty set, so we don't reject every tool node just because we couldn't
        enumerate the catalogue).
        """
        model_manager = ModelManager.for_tenant(tenant_id=tenant_id)
        model_instance = model_manager.get_model_instance(
            tenant_id=tenant_id,
            model_type=ModelType.LLM,
            provider=model_config.provider,
            model=model_config.name,
        )

        model_parameters: dict[str, Any] = dict(model_config.completion_params or {})

        tool_catalogue_text = ""
        installed_tools: set[tuple[str, str]] | None = None
        try:
            entries = build_tool_catalogue(tenant_id)
            tool_catalogue_text = format_tool_catalogue(entries)
            installed_tools = installed_tool_keys(entries)
        except Exception:
            logger.exception("Workflow generator: failed to build tool catalogue for tenant %s", tenant_id)

        return model_instance, model_parameters, tool_catalogue_text, installed_tools
