"""AlbusOS -- framework for building multi-agent systems.

Quick start::

    from albusos import agent, run

    a = agent("researcher", instructions="Research topics.", tools=["web.*"])
    result = await run(a, "What is quantum computing?")

Multi-turn::

    from albusos import agent, Session

    a = agent("researcher", instructions="Research topics.", tools=["web.*"])
    session = Session(a)
    r1 = await session.run("What is quantum computing?")
    r2 = await session.run("Tell me more about qubits")

Custom pathways (power-user)::

    from albusos import AgentBuilder, PathwayBuilder, run

    pathway = (
        PathwayBuilder("qa", pathway_id="qa")
        .llm("answer", "Answer: {{input.goal}}")
        .connect("input", "answer")
        .connect("answer", "output")
        .build()
    )
    a = AgentBuilder().id("qa").pathway("qa").build()
    result = await run(a, "What is 2+2?", pathway=pathway)
"""

from __future__ import annotations

from pathlib import Path

# ---------------------------------------------------------------------------
# Agent runtime
# ---------------------------------------------------------------------------
from core.agent import Agent, AgentRepository, get_agent_repository, set_agent_repository

# ---------------------------------------------------------------------------
# Builders (primary authoring interface)
# ---------------------------------------------------------------------------
from core.builders.agent import AgentBuilder
from core.builders.pathway import PathwayBuilder
from core.builders.skill import SkillBuilder

# ---------------------------------------------------------------------------
# Tool authoring
# ---------------------------------------------------------------------------
from core.pathways.models.tool import ToolOutput

# ---------------------------------------------------------------------------
# Pathway primitives
# ---------------------------------------------------------------------------
from core.pathways.pathway import Pathway, PathwayMode

# ---------------------------------------------------------------------------
# Runner (zero-boilerplate execution)
# ---------------------------------------------------------------------------
from core.runner import Session
from core.runner import run as _core_run
from core.skills.models import Skill, SkillManifest, SkillRequirements
from core.tools import ToolRegistry, get_tool_registry

# ---------------------------------------------------------------------------
# Core types
# ---------------------------------------------------------------------------
from core.types.agents import AgentDefinition, ModelRouting
from core.types.execution import ExecutionBudget, ExecutionMode, ExecutionResult
from core.types.state import ThreadState
from core.types.stream import (
    StreamEvent,
    TokenDelta,
    ToolCallCompleted,
    ToolCallStarted,
    TurnComplete,
)

# ---------------------------------------------------------------------------
# Workflow submission (fire-and-forget)
# ---------------------------------------------------------------------------
from core.workflow import WorkflowJob
from core.workflow import submit as _core_submit
from core.workflow import wait as _core_wait
from core.workspace import WorkspaceInfo

# ---------------------------------------------------------------------------
# MCP client adapter (optional — requires pip install albusos[mcp])
# ---------------------------------------------------------------------------
try:
    from infrastructure.mcp.adapter import connect_mcp
except ImportError:  # mcp package not installed

    async def connect_mcp(*_args, **_kwargs):  # type: ignore[misc]
        """Stub that raises when the ``mcp`` extra is not installed."""
        raise ImportError(
            "MCP client support requires the 'mcp' package. "
            "Install it with: pip install albusos[mcp]"
        )


# ---------------------------------------------------------------------------
# Stdlib bootstrap (opt-in, called automatically by agent() and run())
# ---------------------------------------------------------------------------
from stdlib.bootstrap import load_stdlib

# ---------------------------------------------------------------------------
# State tracking for auto-init
# ---------------------------------------------------------------------------
_stdlib_loaded = False


def _ensure_stdlib() -> None:
    """Load stdlib tools + LLM providers on first use (idempotent)."""
    global _stdlib_loaded
    if not _stdlib_loaded:
        load_stdlib()
        _stdlib_loaded = True


# ---------------------------------------------------------------------------
# agent() — the human-friendly factory
# ---------------------------------------------------------------------------


def agent(
    id: str,
    *,
    instructions: str = "",
    tools: list[str] | None = None,
    model: str = "reasoning",
    name: str | None = None,
    temperature: float = 0.7,
    max_steps: int = 10,
) -> AgentDefinition:
    """Create an agent in one call.

    This is the simplest way to define an agent.  It auto-loads the
    standard tools and LLM providers on first use, and returns an
    ``AgentDefinition`` that ``run()`` / ``Session`` know how to
    execute with the built-in tool-calling loop.

    Parameters
    ----------
    id
        Unique identifier (e.g. ``"researcher"``).
    instructions
        System prompt — tell the agent what it does.
    tools
        Tool patterns the agent can use (e.g. ``["web.*", "memory.*"]``).
        Defaults to ``["*"]`` (all tools).
    model
        Capability name (``"reasoning"``, ``"fast"``, ``"code"``) or
        explicit model string.
    name
        Human-readable display name.  Defaults to *id*.
    temperature
        Sampling temperature.
    max_steps
        Maximum tool-calling rounds per turn.

    Returns
    -------
    AgentDefinition
        Ready to pass to ``run()`` or ``Session``.
    """
    _ensure_stdlib()
    builder = AgentBuilder().id(id).instructions(instructions).model(model)
    builder = builder.temperature(temperature).max_steps(max_steps)
    if name:
        builder = builder.name(name)
    for pattern in tools or ["*"]:
        builder = builder.tool(pattern)
    return builder.build()


# ---------------------------------------------------------------------------
# run() — auto-loads stdlib before executing
# ---------------------------------------------------------------------------


async def run(
    agent_or_definition: Agent | AgentDefinition,
    message: str,
    **kwargs: object,
) -> ExecutionResult:
    """Execute a single agent turn (auto-loads stdlib on first call).

    Accepts the same keyword arguments as ``core.runner.run``:
    ``pathway``, ``thread_id``, ``budget``, ``state_store``, ``mode``.
    """
    _ensure_stdlib()
    return await _core_run(agent_or_definition, message, **kwargs)  # type: ignore[arg-type]


# ---------------------------------------------------------------------------
# submit() / wait() — fire-and-forget workflow execution
# ---------------------------------------------------------------------------


async def submit(
    agent_or_definition: Agent | AgentDefinition,
    inputs: dict[str, object] | str,
    **kwargs: object,
) -> WorkflowJob:
    """Submit a workflow for background execution (auto-loads stdlib).

    Returns a ``WorkflowJob`` that can be polled or awaited via ``wait()``.
    """
    _ensure_stdlib()
    return await _core_submit(agent_or_definition, inputs, **kwargs)  # type: ignore[arg-type]


async def wait(job: WorkflowJob, *, timeout: float = 300) -> ExecutionResult:
    """Block until a workflow job completes.

    Parameters
    ----------
    job
        The ``WorkflowJob`` returned by ``submit()``.
    timeout
        Maximum seconds to wait.

    Returns
    -------
    ExecutionResult
    """
    return await _core_wait(job, timeout=timeout)


# ---------------------------------------------------------------------------
# load_tools() — load custom tool scripts in one call
# ---------------------------------------------------------------------------

_loaded_namespaces: set[str] = set()


def load_tools(
    path: str | Path,
    *,
    namespace: str | None = None,
    instructions: str | None = None,
) -> int:
    """Load tool scripts from a directory into the global registry.

    Each ``.py`` file with an ``async def run()`` function becomes a
    callable tool.  Files prefixed with ``_`` are treated as private
    helpers and skipped.

    If a ``SKILL.md`` file exists in *path* (or its parent), its body is
    used as LLM instructions for the tools — ensuring
    ``SkillRegistry.get_instructions_for_tools()`` works correctly.

    Parameters
    ----------
    path
        Directory containing tool Python files.
    namespace
        Optional prefix for tool names.  When set, a file
        ``list_jobs.py`` is registered as ``"servicem8.list_jobs"``
        (assuming ``namespace="servicem8"``).
    instructions
        Optional instructions for the LLM when using these tools.
        Auto-detected from ``SKILL.md`` if not provided.

    Returns
    -------
    int
        Number of tools loaded.

    Raises
    ------
    FileNotFoundError
        If *path* does not exist.

    Examples
    --------
    ::

        from albusos import load_tools

        load_tools("skills/servicem8/tools", namespace="servicem8")
    """
    _ensure_stdlib()

    resolved = Path(path).resolve()
    if not resolved.exists():
        raise FileNotFoundError(f"Tool directory not found: {path}")

    # Idempotency: skip if this namespace was already loaded.
    dedup_key = f"{namespace}:{resolved}"
    if dedup_key in _loaded_namespaces:
        return 0
    _loaded_namespaces.add(dedup_key)

    from infrastructure.tools.script_tool_loader import load_tools_from_directory

    registry = get_tool_registry()
    results = load_tools_from_directory(resolved)

    tool_names: list[str] = []
    for tool_id, schema, handler in results:
        name = f"{namespace}.{tool_id}" if namespace else tool_id
        registry.register(
            name=name,
            handler=handler,
            description=schema.description or f"Tool: {name}",
            parameters=schema.input_schema,
        )
        tool_names.append(name)

    # Register a Skill so get_instructions_for_tools() works.
    if namespace and tool_names:
        from core.skills.models import Skill
        from core.skills.registry import get_skill_registry

        # Auto-detect instructions from SKILL.md in path or parent.
        if instructions is None:
            for candidate in (resolved / "SKILL.md", resolved.parent / "SKILL.md"):
                if candidate.exists():
                    raw = candidate.read_text(encoding="utf-8")
                    # Strip frontmatter if present.
                    if raw.startswith("---"):
                        _, _, body = raw.split("---", 2)
                        instructions = body.strip()
                    else:
                        instructions = raw.strip()
                    break

        skill = Skill(
            id=namespace,
            name=namespace,
            instructions=instructions or "",
            tools=tool_names,
            tags={"external"},
        )
        get_skill_registry().register(skill)

    return len(results)


# ---------------------------------------------------------------------------
# load_skill() — load a full skill (tools + SKILL.md + registry) in one call
# ---------------------------------------------------------------------------


def load_skill(path: str | Path) -> Skill:
    """Load a skill directory into the global registries.

    Reads ``SKILL.md``, auto-registers Python tools from ``tools/``,
    loads any ``pathways/`` inside the skill, and registers the skill
    into the global ``SkillRegistry`` so that
    ``get_instructions_for_tools()`` works automatically.

    Parameters
    ----------
    path
        Path to the skill directory (must contain ``SKILL.md``).

    Returns
    -------
    Skill
        The loaded and registered skill.

    Raises
    ------
    FileNotFoundError
        If *path* does not exist or contains no ``SKILL.md``.

    Examples
    --------
    ::

        from albusos import load_skill

        load_skill("skills/servicem8")
        load_skill("skills/xero")
    """
    _ensure_stdlib()

    from core.skills.loader import load_skill_from_directory
    from core.skills.registry import get_skill_registry

    resolved = Path(path).resolve()
    if not resolved.exists():
        raise FileNotFoundError(f"Skill directory not found: {path}")

    skill = load_skill_from_directory(resolved)

    if skill is None:
        raise FileNotFoundError(f"No SKILL.md found in {path}")

    skill.tags.add("external")
    get_skill_registry().register(skill)
    return skill


# ---------------------------------------------------------------------------
# load_workspace() — convention-based project auto-discovery
# ---------------------------------------------------------------------------


def load_workspace(root: str | Path = ".") -> WorkspaceInfo:
    """Scan a project root and load skills, pathways, and agents by convention.

    Convention scanning discovers:

    - ``{root}/skills/*/`` — each subdirectory with ``SKILL.md`` is loaded
      via :func:`load_skill`.
    - ``{root}/pathways/`` — each ``.py`` or ``.yaml`` file is loaded as a
      pathway and deployed into the global pathway registry.
    - ``{root}/agents/`` — each ``.py`` file is loaded as an agent
      definition and registered into the global ``AgentRepository``.

    If an ``albus.yaml`` manifest exists at the root, it is used instead
    of convention scanning.

    Parameters
    ----------
    root
        Project root directory.  Defaults to ``"."``.

    Returns
    -------
    WorkspaceInfo
        Summary of what was loaded (skill IDs, pathway IDs, agent IDs).

    Raises
    ------
    FileNotFoundError
        If *root* does not exist.

    Examples
    --------
    ::

        from albusos import load_workspace

        info = load_workspace(".")
        print(info.skills, info.pathways, info.agents)
    """
    _ensure_stdlib()

    from core.workspace import load_workspace as _core_load_workspace

    resolved = Path(root).resolve()
    if not resolved.exists():
        raise FileNotFoundError(f"Workspace root not found: {root}")

    return _core_load_workspace(resolved)


__all__ = [
    "Agent",
    "AgentBuilder",
    "AgentDefinition",
    "AgentRepository",
    "ExecutionBudget",
    "ExecutionMode",
    "ExecutionResult",
    "ModelRouting",
    "Pathway",
    "PathwayBuilder",
    "PathwayMode",
    "Session",
    "Skill",
    "SkillBuilder",
    "SkillManifest",
    "SkillRequirements",
    "StreamEvent",
    "ThreadState",
    "TokenDelta",
    "ToolCallCompleted",
    "ToolCallStarted",
    "ToolOutput",
    "ToolRegistry",
    "TurnComplete",
    "WorkflowJob",
    "WorkspaceInfo",
    "agent",
    "connect_mcp",
    "get_agent_repository",
    "get_tool_registry",
    "load_skill",
    "load_stdlib",
    "load_tools",
    "load_workspace",
    "run",
    "set_agent_repository",
    "submit",
    "wait",
]
