"""Structured execution logging middleware.

This middleware is *pure instrumentation*:
- It does not change execution semantics (no retries, no error remapping).
- It emits one log line per execution attempt (success or error).

It supports both sync and async executors by delegating to `_call_timed_maybe_async`,
which times the call and runs hooks in the appropriate sync/async context.
"""

from __future__ import annotations

import json
import logging
from typing import Generic, TypeVar

from ..execution import BaseExecRequest, Executor
from .base import _call_timed_maybe_async

TReq = TypeVar("TReq", bound=BaseExecRequest)
TOut = TypeVar("TOut")

def _sanitize_param_value(v: object) -> object:
    # Keep numbers/bools as-is.
    if v is None or isinstance(v, (bool, int, float)):
        return v
    if isinstance(v, bytes):
        return f"<bytes len={len(v)}>"

    # SQL params are most commonly strings; avoid dumping huge payloads (e.g. job raw code).
    s = str(v)
    s_stripped = s.strip()

    # Best-effort decode JSON payloads to show keys/important scalars.
    if s_stripped.startswith("{") or s_stripped.startswith("["):
        try:
            parsed = json.loads(s_stripped)
        except Exception:
            parsed = None
        if isinstance(parsed, dict):
            keys = sorted(str(k) for k in parsed.keys())
            out: dict[str, object] = {"json": "object", "keys": keys[:25]}
            # Surface a couple of known useful scalars if present.
            if "auto_suspend_mins" in parsed:
                out["auto_suspend_mins"] = parsed.get("auto_suspend_mins")
            if "timeout_mins" in parsed:
                out["timeout_mins"] = parsed.get("timeout_mins")
            if "readonly" in parsed:
                out["readonly"] = parsed.get("readonly")
            # Record original size to help debugging without leaking full contents.
            out["len"] = len(s)
            if len(keys) > 25:
                out["keys_truncated"] = len(keys) - 25
            return out
        if isinstance(parsed, list):
            return {"json": "array", "len": len(s), "items": len(parsed)}

    if len(s) > 200:
        return f"<str len={len(s)}>"
    return s


def _enriched_meta(req: BaseExecRequest) -> dict[str, object]:
    meta: dict[str, object] = dict(getattr(req, "meta", {}) or {})
    params = getattr(req, "params", None)
    if isinstance(params, list):
        # Gateways often set `meta["params"]` to a count; preserve it as `params_n`.
        existing = meta.get("params")
        if isinstance(existing, int):
            meta["params_n"] = existing
        else:
            meta["params_n"] = len(params)
        meta["params"] = [_sanitize_param_value(p) for p in params]
    return meta


class LoggingMiddleware(Generic[TReq, TOut]):
    """Log execution success/error with timing.

    Logs include:
    - request kind (`req.kind`) and operation name (`req.operation`)
    - elapsed time in ms
    - `req.meta` (caller-supplied structured context)
    """

    def __init__(self, *, logger: logging.Logger | None = None, enabled: bool = True) -> None:
        self._log = logger or logging.getLogger(__name__)
        self._enabled = enabled

    def wrap(self, nxt: Executor[TReq, TOut]) -> Executor[TReq, TOut]:
        log = self._log
        enabled = self._enabled

        class _Wrapped:
            def execute(self, req: TReq) -> TOut:
                if not enabled:
                    return nxt.execute(req)
                # Log what the caller sees: if a downstream policy retries, each attempt
                # will pass through here and be logged separately.
                return _call_timed_maybe_async(
                    lambda: nxt.execute(req),
                    on_success=lambda ms: log.info(
                        "exec_success kind=%s op=%s ms=%.2f meta=%s", req.kind, req.operation, ms, _enriched_meta(req)
                    ),
                    on_error=lambda ms, e: log.error(
                        "exec_error kind=%s op=%s ms=%.2f err=%s meta=%s",
                        req.kind,
                        req.operation,
                        ms,
                        str(e)[:200],
                        _enriched_meta(req),
                    ),
                )

        return _Wrapped()

