"""Shared knowledge / FAQ snapshot for system-prompt injection.

Read docs/profile-isolation/README.md and docs/profile-isolation/technical-design.md
before changing this module.

This layer is intentionally GLOBAL across principal profiles: it lives under the
Hermes root (``~/.hermes/shared_knowledge`` by default), not under an active
profile home. That lets different principals keep isolated private memories
while still seeing the same non-private FAQ / reusable knowledge base.
"""

from __future__ import annotations

from pathlib import Path
from typing import Iterable, Optional

from hermes_constants import get_default_hermes_root
from tools.threat_patterns import scan_for_threats

_ALLOWED_SUFFIXES = {".md", ".markdown", ".txt"}
_DEFAULT_CHAR_LIMIT = 12_000


def resolve_shared_knowledge_dir(path_override: Optional[str] = None) -> Path:
    """Return the global shared-knowledge directory.

    ``path_override`` may be absolute or relative. Relative paths resolve under
    the Hermes root (not the active profile home) so every principal still sees
    the same directory.
    """
    root = get_default_hermes_root()
    raw = str(path_override or "").strip()
    if not raw:
        return root / "shared_knowledge"
    path = Path(raw).expanduser()
    if not path.is_absolute():
        path = root / path
    return path


def iter_shared_knowledge_files(directory: Path) -> Iterable[Path]:
    """Yield supported shared-knowledge files deterministically."""
    if not directory.exists() or not directory.is_dir():
        return []
    files = [
        p for p in directory.rglob("*")
        if p.is_file() and p.suffix.lower() in _ALLOWED_SUFFIXES
    ]
    return sorted(files, key=lambda p: tuple(part.lower() for part in p.relative_to(directory).parts))


def _sanitize_shared_knowledge(content: str, rel_name: str) -> str:
    findings = scan_for_threats(content, scope="context")
    if findings:
        return (
            f"[BLOCKED: {rel_name} contained potential prompt injection "
            f"({', '.join(findings)}). Content not loaded.]"
        )
    return content


def build_shared_knowledge_block(
    *,
    enabled: bool,
    path_override: Optional[str] = None,
    char_limit: Optional[int] = None,
) -> str:
    """Build the shared-knowledge system-prompt block, or ``""`` when absent."""
    if not enabled:
        return ""
    directory = resolve_shared_knowledge_dir(path_override)
    files = list(iter_shared_knowledge_files(directory))
    if not files:
        return ""

    budget = int(char_limit or _DEFAULT_CHAR_LIMIT)
    if budget <= 0:
        budget = _DEFAULT_CHAR_LIMIT

    sections = [
        "## SHARED KNOWLEDGE",
        (
            "Global, non-private FAQ/knowledge shared across principals. Use this for "
            "reusable facts; keep principal-specific preferences and private details in "
            "profile memory instead."
        ),
        f"Path: {directory}",
    ]
    remaining = budget
    added = 0
    omitted = 0

    for path in files:
        rel_name = path.relative_to(directory).as_posix()
        try:
            raw = path.read_text(encoding="utf-8", errors="ignore").strip()
        except OSError:
            omitted += 1
            continue
        if not raw:
            continue
        sanitized = _sanitize_shared_knowledge(raw, rel_name)
        chunk = f"### {rel_name}\n{sanitized}"
        if len(chunk) <= remaining:
            sections.append(chunk)
            remaining -= len(chunk)
            added += 1
            continue
        if remaining > 200:
            trimmed = sanitized[: max(0, remaining - len(rel_name) - 32)].rstrip()
            if trimmed:
                sections.append(f"### {rel_name}\n{trimmed}\n[TRUNCATED]")
                added += 1
        omitted += 1
        break

    if not added:
        return ""
    if omitted > 0 or added < len(files):
        sections.append(
            f"[Shared knowledge truncated: loaded {added} file(s); {len(files) - added} omitted due to size/read limits.]"
        )
    return "\n\n".join(part for part in sections if part).strip()
