"""Observability primitives for PKB.

Provides:
- _configure_logging(): single-call logging setup used by server.py at startup.
- log_tool_call(name): decorator that wraps any tool function with start/ok/err
  log lines and a duration in milliseconds. Works with both sync and async tools.
- redact(value): mask helper for fields that might contain user data or secrets.
- new_trace_id(): short random id for per-tool tracing.
"""

from __future__ import annotations

import asyncio
import functools
import logging
import secrets
import time
from typing import Any, Callable


def _configure_logging(level: int = logging.INFO) -> None:
    """Idempotent logging setup. Safe to call multiple times."""
    if logging.getLogger().handlers:
        return
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(levelname)s %(name)s %(message)s",
    )


def new_trace_id() -> str:
    """8-char hex trace id — short enough to scan in logs, unique enough for a session."""
    return secrets.token_hex(4)


def redact(value: Any, max_len: int = 200) -> str:
    """Return a safe-to-log representation of `value`.

    Strings longer than `max_len` are truncated with a length marker. Bytes
    are summarised by length. Other values pass through `repr` and are then
    length-checked.
    """
    if isinstance(value, str):
        if len(value) > max_len:
            return f"<str len={len(value)} preview={value[:32]!r}...>"
        return value
    if isinstance(value, (bytes, bytearray)):
        return f"<bytes len={len(value)}>"
    s = repr(value)
    if len(s) > max_len:
        return f"<{type(value).__name__} len={len(s)}>"
    return s


def log_tool_call(tool_name: str) -> Callable:
    """Decorator: log start / ok / err for the wrapped tool function.

    Emits three lines max per call: one on entry, one on exit (ok or err).
    Never logs args or return values — only types and lengths. Override per-tool
    if you need richer fields.
    """
    def deco(fn: Callable) -> Callable:
        log = logging.getLogger(f"pkb.tool.{tool_name}")

        if asyncio.iscoroutinefunction(fn):
            @functools.wraps(fn)
            async def awrapper(*args, **kwargs):
                trace = new_trace_id()
                log.info("tool=%s trace=%s phase=start", tool_name, trace)
                t0 = time.monotonic()
                try:
                    result = await fn(*args, **kwargs)
                    log.info(
                        "tool=%s trace=%s phase=ok dur_ms=%.1f result_type=%s",
                        tool_name, trace, (time.monotonic() - t0) * 1000,
                        type(result).__name__,
                    )
                    return result
                except Exception as e:
                    log.warning(
                        "tool=%s trace=%s phase=err dur_ms=%.1f type=%s",
                        tool_name, trace, (time.monotonic() - t0) * 1000,
                        type(e).__name__,
                    )
                    raise
            return awrapper

        @functools.wraps(fn)
        def swrapper(*args, **kwargs):
            trace = new_trace_id()
            log.info("tool=%s trace=%s phase=start", tool_name, trace)
            t0 = time.monotonic()
            try:
                result = fn(*args, **kwargs)
                log.info(
                    "tool=%s trace=%s phase=ok dur_ms=%.1f result_type=%s",
                    tool_name, trace, (time.monotonic() - t0) * 1000,
                    type(result).__name__,
                )
                return result
            except Exception as e:
                log.warning(
                    "tool=%s trace=%s phase=err dur_ms=%.1f type=%s",
                    tool_name, trace, (time.monotonic() - t0) * 1000,
                    type(e).__name__,
                )
                raise
        return swrapper

    return deco
