"""Result formatting functions for search output."""

from __future__ import annotations

import json
import sys
from pathlib import Path, PurePosixPath
from typing import Literal, get_args

OutputFormat = Literal["compact", "verbose", "structured"]
"""Format spec shared by ``mem_search`` and ``mem_agent_search``.

``mem_recall`` uses a 2-format subset (``"compact" | "structured"``) and
intentionally does not import this alias — its surface lacks a semantic
equivalent for ``"verbose"``.
"""

# Membership set derived from OutputFormat so adding a 4th format only
# requires editing the Literal — call-site validators pick it up via
# get_args (drift prevention; see feedback_literal_drives_frozenset.md).
_VALID_OUTPUT_FORMATS: frozenset[str] = frozenset(get_args(OutputFormat))


def _display_path(path) -> str:
    """Return a user-friendly path string.

    On macOS, /tmp is a symlink to /private/tmp. Resolve back to the
    user-facing path so output isn't confusing.
    """
    s = Path(path).as_posix()
    if sys.platform == "darwin" and s.startswith("/private/tmp/"):
        return s[len("/private") :]
    return s


def _short_path(path) -> str:
    """Return just the filename from a path."""
    return PurePosixPath(_display_path(path)).name


def _format_results(results: list, *, verbose: bool = False) -> str:
    """Format search results."""
    parts: list[str] = []
    for r in results:
        parts.append(_format_single_result(r, verbose=verbose))
    return f"Found {len(results)} results:\n\n" + "\n\n".join(parts)


def _format_single_result(r, *, verbose: bool = False) -> str:
    """Format a single SearchResult."""
    if verbose:
        return _format_verbose_result(r)
    return _format_compact_result(r)


def _format_compact_result(r) -> str:
    """Compact format: minimal tokens, no UUID, short path, score 2dp."""
    meta = r.chunk.metadata
    hierarchy = " > ".join(meta.heading_hierarchy) if meta.heading_hierarchy else ""
    ns_badge = f" [{meta.namespace}]" if meta.namespace != "default" else ""
    source = _short_path(meta.source_file)

    header = f"[{r.rank}] {r.score:.2f} |{ns_badge} {source}"
    if hierarchy:
        header += f" > {hierarchy}"

    ctx = getattr(r, "context", None)
    if ctx and (ctx.window_before or ctx.window_after):
        pos_info = f" [{ctx.chunk_position}/{ctx.total_chunks_in_file}]"
        parts = [header + pos_info]
        if ctx.window_before:
            for wc in ctx.window_before:
                parts.append(f"...{wc.content[-200:]}")
        parts.append(r.chunk.content[:500] + ("..." if len(r.chunk.content) > 500 else ""))
        if ctx.window_after:
            for wc in ctx.window_after:
                parts.append(f"{wc.content[:200]}...")
        return "\n".join(parts)

    return header + "\n" + r.chunk.content[:500] + ("..." if len(r.chunk.content) > 500 else "")


def _format_verbose_result(r) -> str:
    """Verbose format: full UUID, full path, score 4dp, code blocks."""
    meta = r.chunk.metadata
    hierarchy = " > ".join(meta.heading_hierarchy) if meta.heading_hierarchy else ""
    ns_badge = f" [{meta.namespace}]" if meta.namespace != "default" else ""

    chunk_id = str(r.chunk.id)
    source = _display_path(meta.source_file)
    header = f"**[{r.rank}]** score={r.score:.4f} | id={chunk_id} |{ns_badge} {source}" + (
        f" | {hierarchy}" if hierarchy else ""
    )

    ctx = getattr(r, "context", None)
    if ctx and (ctx.window_before or ctx.window_after):
        pos_info = f"[chunk {ctx.chunk_position}/{ctx.total_chunks_in_file}]"
        parts = [f"{header} {pos_info}"]
        if ctx.window_before:
            parts.append("--- context before ---")
            for wc in ctx.window_before:
                parts.append(f"...{wc.content[-200:]}")
        parts.append("--- matched ---")
        parts.append(
            f"```\n{r.chunk.content[:500] + ('...' if len(r.chunk.content) > 500 else '')}\n```"
        )
        if ctx.window_after:
            parts.append("--- context after ---")
            for wc in ctx.window_after:
                parts.append(f"{wc.content[:200]}...")
        return "\n".join(parts)

    return (
        header
        + f"\n```\n{r.chunk.content[:500] + ('...' if len(r.chunk.content) > 500 else '')}\n```"
    )


def _format_structured_results(results: list, hints: list[str] | None = None) -> str:
    """JSON structured format for machine consumption.

    Returns a JSON string with all result fields untruncated.
    Unlike compact/verbose, namespace is always included (even "default")
    and content is not clipped to 500 chars.

    When ``hints`` is non-empty, the output also includes a ``hints`` array
    so machine consumers can surface the same trust-UX notices (archive
    filter, embedding mismatch, etc.) that compact/verbose append as text.
    """
    out = []
    for r in results:
        meta = r.chunk.metadata
        hierarchy = " > ".join(meta.heading_hierarchy) if meta.heading_hierarchy else ""
        entry: dict[str, object] = {
            "rank": r.rank,
            "score": round(r.score, 4),
            "source": _short_path(meta.source_file),
            "hierarchy": hierarchy,
            "namespace": meta.namespace,
            "chunk_id": str(r.chunk.id),
            "content": r.chunk.content,
        }
        # RFC P1 Phase C: surface session-summary rescue provenance only
        # when set, so the common case (organic hit) keeps a stable
        # narrow shape and consumers can branch on key presence.
        if getattr(r, "via_session_summary", False):
            entry["via_session_summary"] = True
        out.append(entry)
    payload: dict[str, object] = {"results": out}
    if hints:
        payload["hints"] = list(hints)
    return json.dumps(payload, ensure_ascii=False)


def _format_recall_structured(chunks: list, hints: list[str] | None = None) -> str:
    """JSON structured format for ``mem_recall`` output.

    Recall returns bare ``Chunk`` objects (no rank/score), so this shares the
    shared-meaning field names with ``_format_structured_results`` — namely
    ``chunk_id``, ``namespace``, ``source``, ``hierarchy``, ``content`` — and
    adds ``created_at`` + ``tags`` which are recall-specific. The top-level
    ``kind`` field lets consumers distinguish recall from search payloads on
    the first property; search remains ``kind``-less for backwards compat.

    Matches ``_format_structured_results`` on two invariants:
    * content is untruncated (machine consumer gets the full chunk)
    * ``hints`` key is omitted when the hint list is empty, so clients do not
      render an empty-array UI badge.
    """
    out = []
    for c in chunks:
        meta = c.metadata
        hierarchy = " > ".join(meta.heading_hierarchy) if meta.heading_hierarchy else ""
        out.append(
            {
                "chunk_id": str(c.id),
                "namespace": meta.namespace,
                "source": _short_path(meta.source_file),
                "hierarchy": hierarchy,
                "content": c.content,
                "created_at": c.created_at.isoformat(),
                "tags": list(meta.tags),
            }
        )
    payload: dict[str, object] = {"kind": "recall", "results": out}
    if hints:
        payload["hints"] = list(hints)
    return json.dumps(payload, ensure_ascii=False)
