"""Prompt builder — assembles system prompt from workspace files."""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from pathlib import Path

from qanot.utils import truncate_with_marker

logger = logging.getLogger(__name__)

MAX_FILE_CHARS = 20_000

# ── Plugin registries ──
_plugin_prompt_sections: list[dict] = []  # [{"name": str, "content": str}]
_plugin_template_vars: dict[str, str] = {}


def register_prompt_section(name: str, content: str) -> None:
    """Register a plugin prompt section. Appended after core sections."""
    _plugin_prompt_sections.append({"name": name, "content": content})


def register_template_var(key: str, value: str) -> None:
    """Register a custom template variable for prompt injection.

    Occurrences of ``key`` in the assembled prompt are replaced with ``value``.
    """
    _plugin_template_vars[key] = value


def _truncate_content(content: str, max_chars: int) -> str:
    return truncate_with_marker(content, max_chars)
MAX_TOTAL_CHARS = 150_000

_IDENTITY_LINE = "You are Qanot AI, a personal assistant."

# Marker to split system prompt into cacheable (above) and dynamic (below) parts.
# Anthropic provider uses this to set cache_control on the stable prefix.
_CACHE_BOUNDARY = "<!-- CACHE_BOUNDARY -->"


def build_system_prompt(
    workspace_dir: str = "/data/workspace",
    owner_name: str = "",
    bot_name: str = "",
    timezone_str: str = "Asia/Tashkent",
    context_percent: float = 0.0,
    total_tokens: int = 0,
    skill_path: str | None = None,
    mode: str = "full",
    user_id: str = "",
    skill_index: str = "",
    active_skills_content: str = "",
) -> str:
    """Build the full system prompt from workspace files.

    Args:
        mode: Prompt assembly mode.
            ``"full"``    -- all sections (default).
            ``"minimal"`` -- SOUL.md + TOOLS.md + session info only.
            ``"none"``    -- identity line only.

    Concatenation order (full mode):
    1. SOUL.md (identity, principles)
    2. IDENTITY.md (agent name, vibe, emoji)
    3. SKILL.md (proactive agent behaviors)
    4. TOOLS.md (tool configurations)
    5. AGENTS.md (operating rules)
    6. SESSION-STATE.md (active context)
    7. USER.md excerpt (human context)
    8. BOOTSTRAP.md (first-run ritual, if exists)
    9. SOUL_APPEND.md sections (plugin personality additions)
    """
    if mode == "none":
        return _IDENTITY_LINE

    if mode not in ("full", "minimal"):
        logger.warning("Unknown prompt mode %r, falling back to 'minimal'", mode)
        mode = "minimal"

    ws = Path(workspace_dir)
    parts: list[str] = []
    total_chars = 0

    def _add(content: str) -> None:
        """Append content to parts while tracking total char budget."""
        nonlocal total_chars
        if not content:
            return
        remaining = MAX_TOTAL_CHARS - total_chars
        if remaining <= 0:
            return
        content = _truncate_content(content, min(MAX_FILE_CHARS, remaining))
        parts.append(content)
        total_chars += len(content)

    # 1. SOUL.md
    _add(_read_file(ws / "SOUL.md"))

    if mode == "full":
        # 2. IDENTITY.md (agent name, vibe, emoji)
        _add(_read_file(ws / "IDENTITY.md"))

        # 3. SKILL.md (proactive agent skill)
        if skill_path:
            _add(_read_file(Path(skill_path)))
        else:
            _add(_read_file(ws / "SKILL.md"))

    # 4. TOOLS.md (included in both full and minimal)
    _add(_read_file(ws / "TOOLS.md"))

    if mode == "full":
        # Check for plugin TOOLS files
        for p in sorted(ws.glob("*_TOOLS.md")):
            _add(_read_file(p))

        # 5. AGENTS.md
        _add(_read_file(ws / "AGENTS.md"))

        # 6. USER.md (per-user if exists, fallback to shared) — rarely changes
        user_md = _read_file(ws / "users" / str(user_id) / "USER.md") if user_id else ""
        if not user_md:
            user_md = _read_file(ws / "USER.md")
        _add(user_md)

        # 7. BOOTSTRAP.md — first-run ritual (only if it exists) — static
        if bootstrap := _read_file(ws / "BOOTSTRAP.md"):
            _add(bootstrap)

    # Hardcoded behavioral rules — static
    parts.append(
        "## Tool Call Style\n"
        "Default: do not narrate routine tool calls (just call the tool silently).\n"
        "Narrate only when it helps: multi-step work, complex problems, or when the user explicitly asks.\n"
        "Do not proactively mention internal file names (USER.md, IDENTITY.md, SOUL.md, MEMORY.md, SESSION-STATE.md, etc.).\n"
        "Do not say things like 'I am updating USER.md' or 'Let me save to IDENTITY.md' during routine operations.\n"
        "However, if the user explicitly asks to see or edit these files, comply — they are the owner.\n"
        "When a tool call fails, retry silently or work around it. Never show error details about file operations to the user.\n"
        "All internal bookkeeping is invisible unless the user specifically asks about it."
    )

    # Plugin prompt sections — static (changes only when plugins added/removed)
    for section in _plugin_prompt_sections:
        _add(f"# {section['name']}\n\n{section['content']}")

    # ── CACHE BOUNDARY MARKER ──
    # Everything ABOVE = stable (SOUL, IDENTITY, TOOLS, AGENTS, USER, plugins)
    # Everything BELOW = changes frequently (memory, session state, skills, session info)
    parts.append(_CACHE_BOUNDARY)

    if mode == "full":
        # SESSION-STATE.md — changes on every WAL write (DYNAMIC)
        if state := _read_file(ws / "SESSION-STATE.md"):
            _add(f"# Current Session State\n\n{state}")

        # MEMORY.md — changes when durable facts saved (DYNAMIC)
        if memory := _read_file(ws / "MEMORY.md"):
            _add(f"# Your Long-Term Memory\n\n{memory}")

        # Skill index — may change per turn
        if skill_index:
            _add(skill_index)

        # Active skill content — changes per turn
        if active_skills_content:
            _add(active_skills_content)

    # Session info — changes every request
    now = datetime.now(timezone.utc)
    date_str = now.strftime("%Y-%m-%d")
    time_str = now.strftime("%H:%M UTC")

    parts.append(
        f"# Session Info\n"
        f"- **Date:** {date_str}\n"
        f"- **Time:** {time_str}\n"
        f"- **Timezone:** {timezone_str}\n"
        f"- **Context Usage:** {context_percent:.1f}%\n"
        f"- **Total Tokens:** {total_tokens:,}\n"
        + ("- **WARNING:** Context above 50% — Working Buffer Protocol ACTIVE\n" if context_percent >= 50 else "")
    )

    # Variable injection
    full = "\n\n---\n\n".join(parts)
    full = full.replace("{date}", date_str)
    full = full.replace("{bot_name}", bot_name)
    full = full.replace("{owner_name}", owner_name)
    full = full.replace("{timezone}", timezone_str)

    # Plugin template vars
    for key, value in _plugin_template_vars.items():
        full = full.replace(key, value)

    return full


def _read_file(path: Path) -> str:
    """Read a file if it exists, return empty string otherwise.

    Content is returned as-is; callers (e.g. ``_add``) are responsible
    for applying truncation within the overall character budget.
    """
    try:
        if path.exists():
            return path.read_text(encoding="utf-8").strip()
    except Exception as e:
        logger.warning("Failed to read %s: %s", path, e)
    return ""
