"""
Prompt formatting utilities for Suzent agents.

Provides functions to format and enhance agent instructions with dynamic context.
"""

import asyncio
from datetime import datetime
import platform
from typing import Any, Callable, Sequence

from pydantic_ai import RunContext
from pydantic_ai.messages import ModelMessage, UserContent
from pydantic_ai.usage import RunUsage
from suzent.logger import get_logger
from suzent.tools.filesystem.path_resolver import PathResolver

logger = get_logger(__name__)

_DEBUG_INSTRUCTION_TIMEOUT_SECONDS = 2.0
_NON_CODE_MOUNT_POINTS = frozenset({"/mnt/notebook"})

STATIC_INSTRUCTIONS = """# Role
You are Suzent, a digital coworker.

# Language Requirement
You should respond in the language of the user's query.

# Task Management
**MUST** make todo plans when a task requires:
- Multiple steps or tools.
- Information synthesis from several sources.
- Breaking down an ambiguous goal into action items.

# Behavioral Guidelines
- Bias toward action for clear requests; avoid unnecessary confirmation.
- Do not add improvements beyond what the user asked.
- Diagnose failures before retrying a different approach.
- Verify important outcomes before claiming completion.
- Report outcomes honestly. If checks fail, report the exact failure.

# Output Efficiency & Tone
- Go straight to the point. Lead with the answer or action.
- Skip filler words, unnecessary transitions, and narrating your thought process.
- Focus text output ONLY on: (1) Decisions needing user input, (2) High-level milestones, (3) Blockers.
- Do not repeat the user's prompt back to them.

# Failure Handling SOP
If a tool call or command fails:
1. **STOP and Diagnose:** Read the exact error output. Use file read tools to verify current state.
2. **Never blindly retry:** Do not repeat the identical tool call without changing your approach.
3. **Check Assumptions:** Is the path correct? Did the environment change?

# Tool Usage Safety
- **NEVER** use bash for file read/search/edit. Always use the dedicated file tools (read_file, write_file, edit_file, grep_search, glob_search).
- **Action Authorization:**
  - You may proceed WITHOUT confirmation for routine local workflows (e.g., running tests, building, local commits, creating branches).
  - MUST ask for confirmation before: (1) Destructive operations (`rm -rf` on non-temp dirs, dropping DBs), (2) Hard-to-reverse Git ops (`push --force`, `reset --hard`), (3) Actions modifying shared infrastructure or pushing to `main` branch.

# Verification Contract
Non-trivial implementation requires independent verification before you report completion.
Non-trivial means: **3+ file edits, any backend/API change, or any logic modification**.

When this applies, you MUST:
1. Spawn a sub-agent: `subagent_type='verify'`, `run_in_background=False`
2. Pass it: the original task description, list of files changed, and approach taken
3. Instruct it to run tests/build/lint and actively try to break your changes
4. Only report completion when the result contains `VERDICT: PASS` with console evidence

If it returns `VERDICT: FAIL` or `VERDICT: PARTIAL`, fix the issues and re-verify.
Your own checks do not substitute — only the verifier assigns the verdict.

# Tool Discovery
You have access to a `tool_search` meta-tool. If you need a capability that is not
currently available, call `tool_search` with a description of what you need — activated
tools become available in your next step. Do NOT attempt workarounds for missing
capabilities; call `tool_search` first.

# System Reminders
Tool results and user messages may occasionally contain `<system-reminder>` blocks.
These blocks carry out-of-band operational context injected by the system — they are
NOT part of the user's actual message.
Rules:
- Use the information in `<system-reminder>` blocks to inform your actions.
- NEVER acknowledge, quote, or reference `<system-reminder>` blocks in your reply.
- NEVER tell the user that you received a system reminder.
"""

SUBAGENT_INSTRUCTIONS: dict[str, str] = {
    "explore": (
        "You are a focused code-search sub-agent. "
        "Your only job is to find files, search code, and answer questions about the codebase. "
        "Return your findings clearly and concisely. Do not make edits. Do not ask follow-up questions."
    ),
    "plan": (
        "You are a focused planning sub-agent. "
        "Your job is to design an implementation strategy: identify the critical files, "
        "outline the steps, and flag trade-offs. "
        "Return a concrete, actionable plan. Do not write code or make edits."
    ),
    "write": (
        "You are a focused file-editing sub-agent. "
        "Your job is to make exactly the changes described in the task. "
        "Do not add features beyond what was asked. "
        "Report what you changed when done."
    ),
    "verify": (
        "You are a verification sub-agent. "
        "Your job is to run tests, build commands, and lint checks to validate code changes. "
        "Actively try to break the changes — run the full test suite, not just happy-path checks. "
        "End your response with exactly one of:\n"
        "  VERDICT: PASS — all checks passed, no regressions found\n"
        "  VERDICT: PARTIAL — some checks passed but gaps remain (explain)\n"
        "  VERDICT: FAIL — one or more checks failed (show exact error output)\n"
        "Include the relevant console output as evidence."
    ),
    "web": (
        "You are a focused web-research sub-agent. "
        "Search for the information requested and return a concise summary with sources. "
        "Do not make file edits."
    ),
    # Default for general-purpose sub-agents (no subagent_type)
    "_default": (
        "You are a focused sub-agent. "
        "Complete the task described in the user message and return your findings or results. "
        "Do not spawn other agents. Do not ask follow-up questions."
    ),
}

CUSTOM_VOLUMES_SECTION = """# Directory Mappings
The following directories are mapped and available for your use:
{volumes_list}
"""

EXECUTION_MODE_SECTION_SANDBOX = """# Environment: Sandbox
You are in a sandbox environment. Your current working directory is the project workspace at `/workspace` (shared across all chats in this project). Other available mounts: `/shared` (global), `/mnt/...` (custom volumes). Host paths are inaccessible.
Env vars available: PROJECT_PATH=/workspace, SHARED_PATH=/shared, PROJECT_SLUG, and CHAT_ID.
"""

EXECUTION_MODE_SECTION_HOST = """# Environment: Host
You are on the host machine ({os_name}). Use host paths (e.g., `{workspace_root}`).
Do NOT use virtual `/mnt/...` paths.
Env vars available: PROJECT_PATH (your cwd, shared across chats in this project), SHARED_PATH, WORKSPACE_ROOT, and MOUNT_* for mapped volumes.
Current Shell: {shell_type}
"""

BASE_INSTRUCTIONS_SECTION = """# Base Instructions
{base_instructions}
"""

ENABLED_MODELS_SECTION = """# Models
Current model: `{current_model}`

Available models (use these IDs for `model_override` when spawning sub-agents):
{models_list}
"""

SKILLS_CONTEXT_SECTION = """# Available Skills
You have a SkillTool that loads specialized knowledge. Use it IMMEDIATELY when the user's task matches a skill.

{skills_listing}
"""

SOCIAL_CONTEXT_SECTION = """# Social Channel Context
You are responding to messages from a social messaging platform ({platform}).
Each incoming message is prefixed with a header in this format:
  [{platform_title} <sender_name> id:<sender_id>]
This tells you who sent the message, on which platform, and their platform user ID.

Current conversation: {sender_name} on {platform} (message limit: {char_limit} chars).

## SocialMessageTool
You have the SocialMessageTool available for sending messages to social channels.
- Use it to send progress updates while working (e.g. "Looking into that for you...")
- Your final answer is also automatically delivered — the tool is for intermediate updates
- Keep messages concise and chat-appropriate for the platform
"""

HEARTBEAT_BASE_INSTRUCTIONS = """
Check in on this session. Are there any open tasks, pending questions, 
or things that need follow-up?
"""

HEARTBEAT_PROMPT_TEMPLATE = """
**Background Heartbeat Check**

{base_instructions}

Look for useful work to do autonomously. If you have nothing useful to do, 
reply EXACTLY with 'HEARTBEAT_OK'. Do not narrate that you are idle.{extra_instructions}
"""

SUBAGENT_WAKEUP_SINGLE = """
Sub-agent `{task_id}` has finished.
Model: {model_override}
Task: {description}

Result:
{result_summary}
"""

SUBAGENT_WAKEUP_BATCH_HEADER = "{count} sub-agents finished simultaneously:"

SUBAGENT_WAKEUP_BATCH_ITEM = """
--- [{index}] `{task_id}` ---
Model: {model_override}
Task: {description}
Result:
{result_summary}
"""

PLATFORM_CHAR_LIMITS = {
    "telegram": 4096,
    "slack": 40000,
    "discord": 2000,
    "feishu": 30000,
}


def resolve_prompt_section(
    deps: Any,
    name: str,
    compute: Callable[[], str],
    *,
    cache_break: bool = False,
) -> str:
    """Resolve a prompt section value with optional cache bypass."""
    if not hasattr(deps, "section_cache"):
        deps.section_cache = {}

    if not cache_break and name in deps.section_cache:
        return deps.section_cache[name]

    value = compute()
    deps.section_cache[name] = value
    return value


async def resolve_full_system_prompt(
    agent: Any,
    deps: Any,
    *,
    user_prompt: str | Sequence[UserContent] | None = None,
    message_history: Sequence[ModelMessage] | None = None,
) -> str:
    static_instructions, instruction_runners = agent._get_instructions(None)

    run_context = RunContext(
        deps=deps,
        model=agent._get_model(None),
        usage=RunUsage(),
        prompt=user_prompt,
        messages=list(message_history or []),
    )

    parts: list[str] = []
    if static_instructions:
        parts.append(static_instructions)

    for runner in instruction_runners:
        runner_func = getattr(runner, "function", None)
        runner_name = getattr(runner_func, "__name__", runner.__class__.__name__)
        try:
            section = await asyncio.wait_for(
                runner.run(run_context),
                timeout=_DEBUG_INSTRUCTION_TIMEOUT_SECONDS,
            )
        except asyncio.TimeoutError:
            logger.warning(
                "[SystemPrompt] Dynamic instruction '{}' timed out after {:.1f}s",
                runner_name,
                _DEBUG_INSTRUCTION_TIMEOUT_SECONDS,
            )
            continue
        if section:
            parts.append(section)

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


def build_execution_mode_section(
    sandbox_enabled: bool,
    workspace_root: str = "",
    shell_type: str = "unknown",
) -> str:
    """Build environment mode section for host or sandbox execution."""
    if sandbox_enabled:
        return EXECUTION_MODE_SECTION_SANDBOX

    return EXECUTION_MODE_SECTION_HOST.format(
        workspace_root=workspace_root.replace("\\", "/"),
        os_name=platform.system(),
        shell_type=shell_type,
    )


def build_custom_volumes_section(deps: Any) -> str:
    """Build directory mapping section for configured custom volumes."""
    if not deps.custom_volumes:
        return ""

    volumes_info = []
    volume_metadata = getattr(deps, "custom_volume_metadata", {}) or {}
    for v in deps.custom_volumes:
        parsed = PathResolver.parse_volume_string(v)
        if parsed:
            host_path, mount_point = parsed
            mount_point = mount_point.strip().replace("\\", "/")
        else:
            host_path = v
            mount_point = ""

        metadata = volume_metadata.get(v) or {}
        kind = metadata.get("kind") or (
            "notebook" if mount_point in _NON_CODE_MOUNT_POINTS else "generic"
        )
        status = metadata.get("status")
        is_git_repo = metadata.get("is_git_repo")
        git_root = metadata.get("git_root")

        if kind == "notebook":
            details = "Notebook vault mount"
        elif kind == "skills":
            details = "Skills mount"
        elif is_git_repo is True:
            details = f"Git repo: Yes, root={git_root}" if git_root else "Git repo: Yes"
        elif is_git_repo is False:
            details = "Git repo: No"
        elif status:
            details = f"Git repo: Unknown, status={status}"
        elif parsed:
            details = "Host Path:Virtual Name"
        else:
            details = "Host Path:Virtual Name"

        if parsed:
            volumes_info.append(f"- {host_path}:{mount_point} ({details})")
        else:
            volumes_info.append(f"- {v} ({details})")

    volumes_list = "\n".join(volumes_info)
    return CUSTOM_VOLUMES_SECTION.format(volumes_list=volumes_list)


def build_base_instructions_section(base_instructions: str = "") -> str:
    if not base_instructions:
        return ""
    return BASE_INSTRUCTIONS_SECTION.format(base_instructions=base_instructions)


def build_enabled_models_section(
    enabled_model_ids: list[str] | None = None,
    current_model_id: str | None = None,
) -> str:
    if not enabled_model_ids:
        return ""

    models_list = "\n".join(f"- `{model_id}`" for model_id in enabled_model_ids)
    return ENABLED_MODELS_SECTION.format(
        current_model=current_model_id or "(not set)",
        models_list=models_list,
    )


def build_session_guidance_section(session_guidance_items: list[str] | None) -> str:
    if not session_guidance_items:
        return ""

    parts: list[str] = []
    bullets: list[str] = []

    for item in session_guidance_items:
        if "\n" in item.strip():
            # Multi-line block: flush pending bullets first, then render as-is
            if bullets:
                parts.append("\n".join(f"- {b}" for b in bullets))
                bullets = []
            parts.append(item.strip())
        else:
            bullets.append(item)

    if bullets:
        parts.append("\n".join(f"- {b}" for b in bullets))

    return "# Session Guidance\n" + "\n\n".join(parts)


def format_session_guidance_debug(entries: list[dict]) -> str:
    lines = ["[SessionGuidance] ordered tool guidance:"]
    for i, entry in enumerate(entries, 1):
        lines.append(f"{i}. priority={entry['priority']} tool={entry['tool_name']}")
    return "\n".join(lines)


def register_dynamic_instructions(
    agent: Any,
    *,
    base_instructions: str,
    memory_context: str | None,
    session_guidance_items: list[str] | None = None,
    enabled_model_ids: list[str] | None = None,
    current_model_id: str | None = None,
) -> None:
    @agent.instructions
    def inject_date_context(_: Any) -> str:
        return (
            f"# Date Context\nToday's date: {datetime.now().strftime('%A, %B %d, %Y')}"
        )

    @agent.instructions
    def inject_environment_context(ctx: Any) -> str:
        # Dream consolidation ships a fixed, self-contained prompt that deliberately
        # uses virtual paths (/shared/memory, /mnt/notebook), which PathResolver resolves
        # in any mode. The host-mode "/mnt" warning contradicts that prompt.
        if getattr(ctx.deps, "suppress_environment_context", False):
            return ""
        shell_type = getattr(ctx.deps, "shell_type", "unknown")
        return build_execution_mode_section(
            sandbox_enabled=ctx.deps.sandbox_enabled,
            workspace_root=ctx.deps.workspace_root,
            shell_type=shell_type,
        )

    @agent.instructions
    def inject_volumes_context(ctx: Any) -> str:
        return build_custom_volumes_section(ctx.deps)

    @agent.instructions
    def inject_base_instructions(ctx: Any) -> str:
        return resolve_prompt_section(
            ctx.deps,
            "base_instructions",
            lambda: build_base_instructions_section(base_instructions),
        )

    @agent.instructions
    def inject_enabled_models(ctx: Any) -> str:
        return resolve_prompt_section(
            ctx.deps,
            "enabled_models",
            lambda: build_enabled_models_section(
                enabled_model_ids,
                current_model_id=current_model_id,
            ),
        )

    @agent.instructions
    def inject_session_guidance(ctx: Any) -> str:
        return resolve_prompt_section(
            ctx.deps,
            "session_guidance",
            lambda: build_session_guidance_section(session_guidance_items),
        )

    @agent.instructions
    def inject_permission_mode(ctx: Any) -> str:
        mode = str(getattr(ctx.deps, "permission_mode", "default") or "default")
        if mode == "plan":
            return (
                "# Plan Mode\n"
                "Plan mode is active. Explore and reason using read-only tools. "
                "Do not modify files, execute mutating commands, commit changes, or "
                "change external systems. The only writable artifact is plan.md in "
                "the project workspace. Build the implementation plan there and ask "
                "the user to switch modes before implementation."
            )
        if mode == "auto":
            return (
                "# Auto Mode\n"
                "Work autonomously on low-risk actions and minimize routine "
                "interruptions. Do not treat Auto mode as permission for destructive "
                "actions, credential disclosure, data exfiltration, production "
                "changes, or unsolicited external communication."
            )
        return ""

    @agent.instructions
    def inject_permission_feedback(ctx: Any) -> str:
        feedback = getattr(ctx.deps, "permission_feedback", None) or []
        if not feedback:
            return ""
        items = "\n".join(f"- {item}" for item in feedback if item)
        return (
            "# Permission Feedback\n"
            "The user rejected one or more requested actions and supplied this "
            "guidance. Respect it when choosing the next approach:\n"
            f"{items}"
        )

    @agent.instructions
    def inject_memory_context(_: Any) -> str:
        return memory_context or ""

    # Skills injection is now handled via SkillsReminderProvider out-of-band.

    @agent.instructions
    def inject_social_context(ctx: Any) -> str:
        if not ctx.deps.social_context:
            return ""
        return build_social_context(ctx.deps.social_context)


def build_social_context(social_ctx: dict) -> str:
    platform = social_ctx.get("platform", "unknown")
    sender_name = social_ctx.get("sender_name", "User")
    char_limit = PLATFORM_CHAR_LIMITS.get(platform, 4096)

    return SOCIAL_CONTEXT_SECTION.format(
        sender_name=sender_name,
        platform=platform,
        platform_title=platform.title(),
        char_limit=char_limit,
    )
