"""
Factory Base Utilities

Shared utilities for agent factory functions.
"""

from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from roboco.models import AgentRole, Team


def _get_prompts_base_path() -> Path:
    """Get the base path for prompt layers.

    Resolves to project_root/agents/prompts/ by finding the roboco package
    and going up one level.
    """
    # Start from this file's location: roboco/agents/factories/_base.py
    # Go up: factories -> agents -> roboco -> project_root
    this_file = Path(__file__).resolve()
    project_root = this_file.parent.parent.parent.parent
    prompts_path = project_root / "agents" / "prompts"

    # Fallback: try relative path if above doesn't exist
    if not prompts_path.exists():
        prompts_path = Path("agents/prompts")

    return prompts_path


# Base path for prompt layers
PROMPTS_BASE_PATH = _get_prompts_base_path()


def _load_layer(layer_path: Path) -> str:
    """
    Load a prompt layer file.

    Args:
        layer_path: Path to the layer markdown file

    Returns:
        File content or empty string if not found
    """
    if not layer_path.exists():
        return ""
    return layer_path.read_text().strip()


_ROLE_LAYER_MAP: dict[str, str] = {
    # Cell members
    "developer": "developer.md",
    "qa": "qa.md",
    "documenter": "documenter.md",
    # PMs have separate files
    "main_pm": "main_pm.md",
    "cell_pm": "cell_pm.md",
    # Board members use board layer
    "product_owner": "board.md",
    "head_marketing": "board.md",
    "auditor": "board.md",
    # Intake interviewer — the live-session role prompt that wires up the
    # propose_draft mission. Without this the prompter only sees the
    # gateway verbs layer (i_am_idle) and refuses to draft.
    "prompter": "prompter.md",
    # Secretary — CEO's chief-of-staff live-session role prompt.
    "secretary": "secretary.md",
    # PR reviewer — reviews inbound external/fork PRs, posts one change-request.
    "pr_reviewer": "pr_reviewer.md",
}

_TEAM_LAYER_MAP: dict[str, str] = {
    "backend": "backend.md",
    "frontend": "frontend.md",
    "ux_ui": "ux_ui.md",
}


def _role_layer(prompts_path: Path, role: "AgentRole") -> str | None:
    """Load the role-specific prompt layer, or None if unknown."""
    role_value = role.value if hasattr(role, "value") else str(role)
    role_file = _ROLE_LAYER_MAP.get(role_value)
    if not role_file:
        return None
    return _load_layer(prompts_path / "roles" / role_file)


def _team_layer(prompts_path: Path, team: "Team | None") -> str | None:
    """Load the team-specific prompt layer, or None if unset/unknown."""
    if not team:
        return None
    team_value = team.value if hasattr(team, "value") else str(team)
    team_file = _TEAM_LAYER_MAP.get(team_value)
    if not team_file:
        return None
    return _load_layer(prompts_path / "teams" / team_file)


def _autogen_verbs_layer(prompts_path: Path, role: "AgentRole") -> str | None:
    """Load the autogenerated verb signatures for this role.

    The file at ``_generated/<role>.md`` is rebuilt by
    ``scripts/regenerate_verb_tables.py`` from the Pydantic schemas in
    ``roboco/api/schemas/v1/`` plus the role-config. Including it as a
    composition layer means the prompt always shows the literal accepted
    body shape for every verb the agent has — eliminating the prompt
    drift class where the prompt and the accepted body shape diverged.
    """
    role_value = role.value if hasattr(role, "value") else str(role)
    return _load_layer(prompts_path / "_generated" / f"{role_value}.md")


# Built-in Claude Code tools each role's session needs at spawn time.
# Dogfooding surfaced: a developer hit "Edit exists but is not enabled in
# this context" because Claude Code v2.1.69+ defers built-in tools behind a
# ToolSearch activation. Weak models skip the soft directive in the
# briefing, so we hoist the exact call into a top-of-system-prompt
# layer (highest-priority instruction the model sees).
#
# Read/Bash/Grep/Glob/TodoWrite are needed by every role; Edit/Write
# only by roles that author code or docs.
_BUILTIN_TOOLS_COMMON: tuple[str, ...] = (
    "Read",
    "Bash",
    "Grep",
    "Glob",
    "TodoWrite",
)
_BUILTIN_TOOLS_AUTHORS: tuple[str, ...] = (*_BUILTIN_TOOLS_COMMON, "Edit", "Write")

_ROLE_BUILTIN_TOOLS: dict[str, tuple[str, ...]] = {
    "developer": _BUILTIN_TOOLS_AUTHORS,
    "documenter": _BUILTIN_TOOLS_AUTHORS,
    "qa": _BUILTIN_TOOLS_COMMON,
    "main_pm": _BUILTIN_TOOLS_COMMON,
    "cell_pm": _BUILTIN_TOOLS_COMMON,
    "product_owner": _BUILTIN_TOOLS_COMMON,
    "head_marketing": _BUILTIN_TOOLS_COMMON,
    "auditor": _BUILTIN_TOOLS_COMMON,
    "pr_reviewer": _BUILTIN_TOOLS_COMMON,
}


def _tool_load_directive_layer(role: "AgentRole") -> str | None:
    """Top-of-prompt statement that built-in tools are ready to use.

    Built-in tools are pre-loaded at spawn via the Claude Code `--tools`
    flag and gated only by the per-role permission allow/deny rules — NOT
    by ToolSearch (which exists only for deferred MCP tools, never for
    built-ins). An earlier revision instructed agents to "run ToolSearch
    to activate Edit/Write"; that was false (ToolSearch is not a built-in
    activator and is not even a callable tool here), so weak models
    chased a nonexistent tool, concluded Edit/Write were unavailable, and
    rewrote whole files via destructive shell redirection. This block
    instead affirms the tools are live and steers away from that
    failure: enumerated tools, used directly, edits via Edit/Write.
    """
    role_value = role.value if hasattr(role, "value") else str(role)
    tools = _ROLE_BUILTIN_TOOLS.get(role_value)
    if not tools:
        return None
    tool_list = ", ".join(tools)
    authors = "Edit" in tools
    edit_line = (
        "Make file changes with Edit/Write — never rewrite a whole file "
        "via shell redirection (>, heredoc, tee); that destroys content "
        "and is unnecessary.\n"
        if authors
        else "You read and review; you do not author files.\n"
    )
    return (
        "# Your tools are ready\n"
        "\n"
        f"These built-in tools are loaded and available now: {tool_list}.\n"
        "Use them directly. Do NOT call ToolSearch — it does not gate "
        "built-in tools and is not available here; calling it only wastes "
        "budget.\n"
        f"{edit_line}"
    )


def _lifecycle_layer(prompts_path: Path, role: "AgentRole") -> str | None:
    """Load the canonical lifecycle fragment for this role.

    The file at ``_generated/lifecycle-<role>.md`` is regenerated from
    ``roboco/lifecycle/spec.py`` by ``scripts/build_lifecycle_artifacts.py``
    (``make lifecycle``). It lists exactly the verbs the role can invoke
    with one-line descriptions — the canonical source of truth that the
    gateway enforces. Placing it at the top of the system prompt means
    the agent reads its allowed verb surface before anything else. CI
    gates on ``make lifecycle && git diff --exit-code`` so this fragment
    cannot drift from the spec.
    """
    role_value = role.value if hasattr(role, "value") else str(role)
    return _load_layer(prompts_path / "_generated" / f"lifecycle-{role_value}.md")


def compose_prompt(
    role: "AgentRole",
    team: "Team | None",
    agent_slug: str,
    base_path: Path | None = None,
) -> str:
    """
    Compose a system prompt from layered components.

    Combines:
    1. _generated/lifecycle-{role}.md - Canonical lifecycle verb surface (from spec)
    2. base.md - Universal rules (all agents)
    3. roles/{role}.md - Role-specific behavior
    4. _generated/{role}.md - Autogenerated verb-signature table from schemas
    5. teams/{team}.md - Team context (if team is set)
    6. identities/{agent_slug}.md - Agent identity

    The lifecycle fragment goes first so every agent reads its allowed
    verb surface before any other instruction. It is regenerated from
    ``roboco/lifecycle/spec.py`` by ``make lifecycle`` and gated on CI.

    Args:
        role: Agent's role (developer, qa, pm, documenter, board)
        team: Agent's team (backend, frontend, ux_ui, or None for board)
        agent_slug: Agent's slug identifier (e.g., "be-dev-1")
        base_path: Optional override for prompts base path

    Returns:
        Composed system prompt string
    """
    prompts_path = base_path or PROMPTS_BASE_PATH
    parts: list[str] = []

    for layer in (
        _tool_load_directive_layer(role),
        _lifecycle_layer(prompts_path, role),
        _load_layer(prompts_path / "base.md"),
        _role_layer(prompts_path, role),
        _autogen_verbs_layer(prompts_path, role),
        _team_layer(prompts_path, team),
        _load_layer(prompts_path / "identities" / f"{agent_slug}.md"),
    ):
        if layer:
            parts.append(layer)

    # Join with separator
    return "\n\n---\n\n".join(parts)


def make_slug(name: str) -> str:
    """Convert a name to a URL-safe slug."""
    return name.lower().replace(" ", "-")
