"""Per-file manifest of the context/instruction files behind the ``/context`` "Rules" figure.

Read-only: enumerates the same candidates ``build_context_files_prompt`` loads (through
``agent.prompt_builder.discover_context_files`` — one discovery walk, so the listing cannot drift from the
prompt) and reports, per file, its size and whether it was loaded, truncated over the context-file cap,
shadowed by a higher-priority context type, blocked by the injection scan (or, for the user's own SOUL.md,
flagged but loaded), empty/unreadable, or suppressed by the install-tree guard. Nothing here builds a prompt or touches the truncation-warning ContextVar, so it
is free of cache impact.

Approximations (the manifest re-derives, it does not re-render): the truncation check sizes the raw
``## label`` section, so a .hermes.md whose YAML frontmatter the builder strips can read a few chars larger
here, and the AGENTS.md directory-chain cap (applied to the merged chain after per-file caps) is not modelled.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any, Dict, List, Optional

from agent import prompt_builder as _pb
from agent.model_metadata import estimate_tokens_rough

# status -> (glyph, note shown after the token count; "" = none)
_STATUS_DISPLAY = {
    "loaded": ("✓", ""),
    "truncated": ("◐", "truncated — over context_file_max_chars"),
    "shadowed": ("○", "not loaded — higher-priority context type wins"),
    "blocked": ("✗", "not loaded — blocked by the prompt-injection scan"),
    "flagged": ("⚠", "loaded — matched prompt-injection pattern(s); review the file"),
    "empty": ("○", "not loaded — empty file"),
    "unreadable": ("✗", "not loaded — could not be read"),
    "suppressed": ("○", "not loaded — cwd fell back to the Hermes install tree"),
}


def _entry(label: str, path: Path, content: str, status: str) -> Dict[str, Any]:
    return {
        "label": label, "path": str(path), "chars": len(content), "est_tokens": estimate_tokens_rough(content),
        "loaded": status in ("loaded", "truncated", "flagged"), "status": status,
    }


def _empty_status(path: Path) -> str:
    """A file the builder read as "" is either genuinely empty or unreadable (permissions, timeout)."""
    try:
        return "unreadable" if path.stat().st_size > 0 else "empty"
    except OSError:
        return "unreadable"


def _loaded_status(content: str, rendered_len: int, max_chars: int, user_authored: bool = False) -> str:
    """Same scan the builder runs (``_scan_context_content``): a hit replaces a project file with a BLOCKED
    marker; the user's own SOUL.md (*user_authored*) still loads and is reported as ``flagged``."""
    if _pb._scan_for_threats(content.lstrip("\ufeff"), scope="context"):
        return "flagged" if user_authored else "blocked"
    return "truncated" if rendered_len > max_chars else "loaded"


def list_context_file_sources(
    cwd: Optional[str] = None, context_length: Optional[int] = None, allow_install_tree_fallback: bool = False,
    home_override: "Path | None" = None, skip_soul: bool = False,
) -> List[Dict[str, Any]]:
    """One dict per context file Hermes considered, in the builder's priority order.

    Same signature semantics as ``build_context_files_prompt`` (``cwd=None`` → launch dir, install-tree guard
    unless *allow_install_tree_fallback*). Keys: ``label``, ``path``, ``chars``, ``est_tokens``, ``loaded``
    and ``status`` ∈ loaded / truncated / flagged / shadowed / blocked / empty / unreadable / suppressed.
    """
    cwd_path = Path(cwd if cwd is not None else os.getcwd()).resolve()
    max_chars = _pb._get_context_file_max_chars(context_length)
    suppressed = _pb._project_context_suppressed(cwd, cwd_path, allow_install_tree_fallback)
    sources: List[Dict[str, Any]] = []
    winner: Optional[str] = None
    for kind, label, path, content in _pb.discover_context_files(cwd_path):
        if not content:
            status = _empty_status(path)
        elif suppressed:
            status = "suppressed"
        elif winner in (None, kind):
            winner = kind
            # The builder caps the rendered ``## label`` section, not the raw file.
            status = _loaded_status(content, len(f"## {label}\n\n{content}"), max_chars)
        else:
            status = "shadowed"
        sources.append(_entry(label, path, content, status))

    if not skip_soul:
        home = Path(home_override) if home_override is not None else _pb.get_hermes_home()
        soul_path = home / "SOUL.md"
        if _pb._exists_or_denied(soul_path):
            content = _pb._read_context_file(soul_path)
            status = (_loaded_status(content, len(content), max_chars, user_authored=True) if content
                      else _empty_status(soul_path))
            sources.append(_entry("SOUL.md", soul_path, content, status))
    return sources


def context_file_sources_for_agent(agent: Any) -> List[Dict[str, Any]]:
    """The manifest for a live agent, resolved exactly like ``agent.system_prompt._context_files_part``
    (session cwd, install-tree policy per platform, the agent's own profile home)."""
    if getattr(agent, "skip_context_files", False):
        return []
    from agent.runtime_cwd import resolve_context_cwd
    from agent.system_prompt import _agent_home
    launch_artifact = getattr(agent, "_context_cwd_is_launch_artifact", False)
    cwd = resolve_context_cwd(include_session_override=not launch_artifact)
    ctx_len = getattr(getattr(agent, "context_compressor", None), "context_length", None)
    return list_context_file_sources(
        cwd=str(cwd) if cwd is not None else None, context_length=ctx_len if isinstance(ctx_len, int) else None,
        allow_install_tree_fallback=getattr(agent, "platform", None) in ("cli", "tui"), home_override=_agent_home(agent),
    )


def render_context_file_lines(sources: List[Dict[str, Any]]) -> List[str]:
    """Plain-text ``Context files`` block for ``/context``; [] when nothing was found."""
    if not sources:
        return []
    width = max(len(str(src.get("label") or "")) for src in sources)
    lines = ["Context files"]
    for src in sources:
        glyph, note = _STATUS_DISPLAY.get(str(src.get("status") or ""), ("•", ""))
        suffix = f"  ({note})" if note else ""
        lines.append(f"{glyph} {str(src.get('label') or ''):<{width}} ~{int(src.get('est_tokens') or 0):>9,} tokens{suffix}")
    return lines
