"""Child prompt construction.

Assembles the prompt sent to each ``claude --print`` child from the task file,
manifest context, repo CLAUDE.md, sibling coordination data, and the
orchestrator contract.

Prompt injection mitigation: The contract is placed both before AND after
user-controlled content (sandwich pattern). Each user-controlled section
is size-capped and wrapped in explicit delimiters to reduce the risk of
adversarial content overriding orchestrator instructions.
"""

from __future__ import annotations

import contextlib
import logging
from pathlib import Path

logger = logging.getLogger(__name__)

# Maximum characters for each user-controlled prompt section.
# Prevents context window exhaustion and keeps the contract visible.
_MAX_CLAUDE_MD_CHARS = 8000
_MAX_MANIFEST_CHARS = 4000
_MAX_SIBLINGS_CHARS = 4000
_MAX_TASK_FILE_CHARS = 12000
_MAX_PLANNER_CLAUDE_MD_CHARS = 6000

# Contract appended to every child prompt, defining how the child must
# report its results so the orchestrator can parse them.
#
# Dynamic fields ({task_file_path}, {workspace_path}, {timeout_seconds},
# {budget_usd}) are filled in by build_child_prompt().
CHILD_CONTRACT_TEMPLATE = """\

## Orchestrator Contract

You are a child agent managed by the Cleave orchestrator. Follow these rules:

1. **Scope**: Only work on the task described above. Do not modify files outside your scope.
2. **Task file**: Update your task file at `{task_file_path}` when done:
   - In the Result section, set `status:` to exactly one of: SUCCESS, PARTIAL, FAILED, or NEEDS_DECOMPOSITION
   - Fill in `summary:`, `files_modified:`, `interfaces_published:`, `decisions_made:`
   - The orchestrator reads this file to determine your outcome. If you do not update it,
     your work may not be recognized.
3. **NEEDS_DECOMPOSITION**: If the task is too large or complex to complete in one pass,
   set status to NEEDS_DECOMPOSITION and describe in the summary what sub-tasks are needed.
   The orchestrator will recursively decompose and re-dispatch.
4. **Commits**: Commit your work to the current branch with clear commit messages.
   Do not push — the orchestrator handles branch management.
5. **No side effects**: Do not install global packages, modify system state, or access
   the network beyond what the task requires.
6. **Verification**: Run any tests or checks specified in the success criteria and
   report results in the Verification section of your task file.

### Environment Constraints

- **Available tools**: Bash, Edit, Read, Write, Glob, Grep (no others)
- **No MCP servers**: You have no access to Graphiti, Sequential Thinking, Slack, Notion, or any MCP services
- **No skills or plugins**: /cleave and other skills are disabled
- **No web access tools**: WebFetch and WebSearch are unavailable (curl via Bash is possible but discouraged)
- **No subagents**: The Task tool is not available; you cannot spawn child processes
- **Budget**: ${budget_usd:.2f} maximum for this task
- **Timeout**: {timeout_seconds}s — you will be terminated (SIGTERM then SIGKILL) if you exceed this
- **Workspace directory**: `{workspace_path}`
"""


def _truncate(content: str, max_chars: int, label: str) -> str:
    """Truncate content with a warning if it exceeds the limit."""
    if len(content) <= max_chars:
        return content
    logger.warning(
        "%s content truncated from %d to %d chars",
        label, len(content), max_chars,
    )
    return content[:max_chars] + f"\n\n[... truncated at {max_chars} chars]"


def build_child_prompt(
    task_file_path: Path,
    manifest_path: Path | None = None,
    claude_md_path: Path | None = None,
    siblings_path: Path | None = None,
    success_criteria: list[str] | None = None,
    workspace_path: Path | None = None,
    child_timeout_seconds: int = 7200,
    child_budget_usd: float = 15.0,
    root_directive: str | None = None,
) -> str:
    """Build the full prompt for a child claude --print invocation.

    Args:
        task_file_path: Path to the child's N-task.md file.
        manifest_path: Path to manifest.yaml (optional context).
        claude_md_path: Path to repo CLAUDE.md (optional context).
        siblings_path: Path to siblings.yaml (sibling coordination context).
        success_criteria: Global success criteria to include.
        workspace_path: Absolute path to the .cleave-* workspace directory.
        child_timeout_seconds: Per-child timeout in seconds.
        child_budget_usd: Per-child budget in USD.
        root_directive: The original top-level directive, preserved across
            recursion depth. Injected as a Root Goal section when it differs
            from the task-level directive.

    Returns:
        The assembled prompt string.
    """
    # Build the contract (used at both top and bottom — sandwich pattern)
    contract = CHILD_CONTRACT_TEMPLATE.format(
        task_file_path=task_file_path,
        workspace_path=workspace_path or "(unknown)",
        timeout_seconds=child_timeout_seconds,
        budget_usd=child_budget_usd,
    )

    sections: list[str] = []

    # CONTRACT FIRST — establishes authority before any user content
    sections.append(contract)

    # --- User-controlled sections (size-capped, delimited) ---

    # Repo CLAUDE.md for codebase conventions
    if claude_md_path and claude_md_path.exists():
        try:
            content = claude_md_path.read_text(encoding="utf-8").strip()
        except OSError:
            content = ""
        if content:
            content = _truncate(content, _MAX_CLAUDE_MD_CHARS, "CLAUDE.md")
            sections.append(
                "## Repository Context (CLAUDE.md)\n\n"
                "<!-- BEGIN REPO CONTEXT -->\n"
                f"{content}\n"
                "<!-- END REPO CONTEXT -->"
            )

    # Manifest context
    if manifest_path and manifest_path.exists():
        try:
            content = manifest_path.read_text(encoding="utf-8").strip()
        except OSError:
            content = ""
        if content:
            content = _truncate(content, _MAX_MANIFEST_CHARS, "manifest")
            sections.append(
                "## Workspace Manifest\n\n"
                f"```yaml\n{content}\n```"
            )

    # Sibling coordination context
    if siblings_path and siblings_path.exists():
        try:
            content = siblings_path.read_text(encoding="utf-8").strip()
        except OSError:
            content = ""
        if content:
            content = _truncate(content, _MAX_SIBLINGS_CHARS, "siblings")
            sections.append(
                "## Sibling Coordination\n\n"
                "The following siblings are executing in parallel. Respect their "
                "scope boundaries and coordinate on shared interfaces.\n\n"
                f"```yaml\n{content}\n```"
            )

    # Root goal — preserved across recursion depth so children at any
    # level can verify alignment with the original intent.
    if root_directive:
        # Read task file content to check if directive differs
        task_content = ""
        if task_file_path.exists():
            with contextlib.suppress(OSError):
                task_content = task_file_path.read_text(encoding="utf-8").strip()
        # Only inject if root_directive differs from what the child sees
        if root_directive not in task_content:
            sections.append(
                "## Root Goal\n\n"
                "The original top-level directive (preserved across all "
                "decomposition levels):\n\n"
                f"> {root_directive}"
            )

    # Task file — the primary instruction
    if task_file_path.exists():
        try:
            content = task_file_path.read_text(encoding="utf-8").strip()
        except OSError:
            content = ""
        if content:
            content = _truncate(content, _MAX_TASK_FILE_CHARS, "task file")
        sections.append(f"## Your Task\n\n{content}" if content
                        else "## Your Task\n\n(Task file empty)")
    else:
        logger.warning("Task file not found: %s", task_file_path)
        sections.append("## Your Task\n\n(Task file not found — check orchestrator state)")

    # Success criteria
    if success_criteria:
        criteria_text = "\n".join(f"- {c}" for c in success_criteria)
        sections.append(f"## Success Criteria\n\n{criteria_text}")

    # CONTRACT AGAIN — sandwich closing ensures it's in recency window
    sections.append(
        "## REMINDER: Orchestrator Contract\n\n"
        "The rules in the Orchestrator Contract section above are binding. "
        "Update your task file with the correct status when done. "
        "Stay within your scope. Do not push branches."
    )

    return "\n\n".join(sections)


def build_planner_prompt(
    directive: str,
    repo_path: Path,
    success_criteria: list[str] | None = None,
    allowed_tools: str | None = None,
    child_timeout_seconds: int | None = None,
    child_budget_usd: float | None = None,
) -> str:
    """Build the prompt for the planning phase (split strategy).

    The planner uses a lighter model to analyze the directive and produce
    a JSON split strategy.

    Args:
        directive: The top-level task directive.
        repo_path: Path to the target repository.
        success_criteria: Overall success criteria.
        allowed_tools: Tool allowlist for children (informs feasibility).
        child_timeout_seconds: Per-child timeout (informs task sizing).
        child_budget_usd: Per-child budget (informs task sizing).

    Returns:
        The planner prompt string.
    """
    criteria_block = ""
    if success_criteria:
        criteria_block = "\n\nSuccess criteria:\n" + "\n".join(f"- {c}" for c in success_criteria)

    # Read repo structure for context (top-level only)
    tree_lines: list[str] = []
    try:
        for item in sorted(repo_path.iterdir()):
            if item.name.startswith("."):
                continue
            prefix = "dir " if item.is_dir() else "    "
            tree_lines.append(f"  {prefix}{item.name}")
    except OSError:
        tree_lines.append("  (unable to list directory)")

    tree_block = "\n".join(tree_lines[:50])  # cap at 50 entries

    # Read CLAUDE.md if present (size-capped)
    claude_md = repo_path / "CLAUDE.md"
    claude_context = ""
    if claude_md.exists():
        try:
            text = claude_md.read_text(encoding="utf-8").strip()
        except OSError:
            text = ""
        if text:
            text = _truncate(text, _MAX_PLANNER_CLAUDE_MD_CHARS, "planner CLAUDE.md")
            claude_context = f"\n\nRepository CLAUDE.md:\n{text}\n"

    # Child capability constraints block
    tools_str = allowed_tools or "Bash Edit Read Write Glob Grep"
    timeout_str = f"{child_timeout_seconds}s" if child_timeout_seconds else "2h"
    budget_str = f"${child_budget_usd:.2f}" if child_budget_usd else "$15.00"

    capability_block = f"""
Child execution constraints (each child runs as an isolated subprocess):
- Available tools: {tools_str}
- No MCP servers, skills, plugins, web search, or subagent capability
- Per-child budget: {budget_str}
- Per-child timeout: {timeout_str}
- Each child works in its own git worktree branch
- Children cannot communicate with each other during execution"""

    return f"""\
Analyze this directive and produce a JSON split strategy for parallel execution.

Directive: {directive}{criteria_block}

Repository: {repo_path.name}
{claude_context}
Top-level structure:
{tree_block}
{capability_block}

Respond with ONLY a JSON object (no markdown fences, no explanation) matching this schema:

{{
  "children": [
    {{
      "label": "short-kebab-case-label",
      "description": "What this child task should accomplish",
      "scope": ["list", "of", "file/dir", "patterns", "this", "child", "owns"],
      "depends_on": ["label-of-child-that-must-finish-first"]
    }}
  ],
  "rationale": "Brief explanation of why this decomposition makes sense"
}}

Rules:
- 2-4 children, executed in parallel where possible
- Use depends_on to express sequential ordering when one child's output feeds another (e.g., schema migration before API code). Omit or leave empty when children are independent
- Children should have minimal file overlap
- Each child must be completable with only filesystem tools and Bash (no web, no MCP, no skills)
- Labels must be unique, kebab-case, max 40 chars
- Scope patterns use glob syntax (e.g., "src/auth/**", "tests/test_auth*")
"""
