"""
Recorders: record_llm_call, record_tool_call, record_state.
Depends: agentdbg.events, agentdbg.storage, agentdbg.loopdetect, _redact, _context.
"""

from typing import Any

from agentdbg.config import AgentDbgConfig
from agentdbg.events import EventType, new_event
from agentdbg.exceptions import AgentDbgLoopAbort
from agentdbg.loopdetect import detect_loop, pattern_key as loop_pattern_key

from agentdbg._tracing._context import (
    _append_event_and_check_guardrails,
    _ensure_run,
    _guardrail_params_var,
)
from agentdbg._tracing._redact import (
    _apply_redaction_truncation,
    _build_error_payload,
    _normalize_usage,
)


def _maybe_emit_loop_warning(
    run_id: str,
    counts: dict[str, int],
    config: AgentDbgConfig,
    window: list[dict],
    emitted: set[str],
) -> None:
    """
    If the last N events contain a repeating pattern not yet emitted, emit LOOP_WARNING,
    increment counts["loop_warnings"], and add the pattern key to emitted.
    """
    payload = detect_loop(window, config.loop_window, config.loop_repetitions)
    if payload is None:
        return
    key = loop_pattern_key(payload)
    if key in emitted:
        # Pattern already emitted — no duplicate event.  But if stop_on_loop
        # is active the previous AgentDbgLoopAbort may have been swallowed by
        # the calling framework (e.g. LangChain, OpenAI Agents SDK).  Re-raise
        # so the loop keeps being interrupted on every detection opportunity.
        params = _guardrail_params_var.get()
        if params is not None and params.stop_on_loop:
            repetitions = payload.get("repetitions", 0)
            if repetitions >= params.stop_on_loop_min_repetitions:
                raise AgentDbgLoopAbort(
                    threshold=params.stop_on_loop_min_repetitions,
                    actual=repetitions,
                    message=(
                        f"guardrail stop_on_loop: loop re-detected after previous "
                        f"abort was swallowed (repetitions {repetitions} >= "
                        f"stop_on_loop_min_repetitions "
                        f"{params.stop_on_loop_min_repetitions})"
                    ),
                )
        return
    pattern = payload.get("pattern", "loop_warning")
    max_name_len = 80
    name = (
        pattern if len(pattern) <= max_name_len else pattern[: max_name_len - 1] + "..."
    )
    ev = new_event(EventType.LOOP_WARNING, run_id, name, payload)
    # Record the dedup key and increment the count BEFORE the guardrail
    # check.  _append_event_and_check_guardrails may raise AgentDbgLoopAbort
    # when stop_on_loop is enabled; if the calling framework swallows that
    # exception (e.g. OpenAI Agents SDK), subsequent spans would re-trigger
    # the same detection and emit duplicate LOOP_WARNINGs.
    emitted.add(key)
    counts["loop_warnings"] = counts.get("loop_warnings", 0) + 1
    _append_event_and_check_guardrails(run_id, ev, config, counts)


def record_llm_call(
    model: str,
    prompt: Any = None,
    response: Any = None,
    usage: Any = None,
    meta: dict[str, Any] | None = None,
    provider: str = "unknown",
    temperature: Any = None,
    stop_reason: str | None = None,
    status: str = "ok",
    error: str | BaseException | dict[str, Any] | None = None,
) -> None:
    """
    Record an LLM call event. No-op if no active run (unless AGENTDBG_IMPLICIT_RUN=1).
    Applies redaction and truncation from config, appends event, increments llm_calls.
    When status is "error", error may be an exception, string, or dict (type, message, details?, stack?).
    """
    ctx = _ensure_run()
    if ctx is None:
        return
    run_id, counts, config, window, emitted = ctx
    status_val = "ok" if status not in ("ok", "error") else status
    error_obj: dict[str, Any] | None = None
    if status_val == "error" and error is not None:
        error_obj = _build_error_payload(error, config, include_stack=True)
    payload = {
        "model": model,
        "prompt": prompt,
        "response": response,
        "usage": _normalize_usage(usage),
        "provider": provider,
        "temperature": temperature,
        "stop_reason": stop_reason,
        "status": status_val,
        "error": error_obj,
    }
    payload, safe_meta = _apply_redaction_truncation(payload, meta or {}, config)
    ev = new_event(EventType.LLM_CALL, run_id, model, payload, meta=safe_meta)
    counts["llm_calls"] = counts.get("llm_calls", 0) + 1
    _append_event_and_check_guardrails(run_id, ev, config, counts)
    window.append(ev)
    if len(window) > config.loop_window:
        window[:] = window[-config.loop_window :]
    _maybe_emit_loop_warning(run_id, counts, config, window, emitted)


def record_tool_call(
    name: str,
    args: Any = None,
    result: Any = None,
    meta: dict[str, Any] | None = None,
    status: str = "ok",
    error: str | BaseException | dict[str, Any] | None = None,
) -> None:
    """
    Record a tool call event. No-op if no active run (unless AGENTDBG_IMPLICIT_RUN=1).
    Applies redaction and truncation, appends event, increments tool_calls.
    When status is "error", error may be an exception, string, or dict (type, message, details?, stack?).
    """
    ctx = _ensure_run()
    if ctx is None:
        return
    run_id, counts, config, window, emitted = ctx
    status_val = "ok" if status not in ("ok", "error") else status
    error_obj: dict[str, Any] | None = None
    if status_val == "error" and error is not None:
        error_obj = _build_error_payload(error, config, include_stack=True)
    payload = {
        "tool_name": name,
        "args": args,
        "result": result,
        "status": status_val,
        "error": error_obj,
    }
    payload, safe_meta = _apply_redaction_truncation(payload, meta or {}, config)
    ev = new_event(EventType.TOOL_CALL, run_id, name, payload, meta=safe_meta)
    counts["tool_calls"] = counts.get("tool_calls", 0) + 1
    _append_event_and_check_guardrails(run_id, ev, config, counts)
    window.append(ev)
    if len(window) > config.loop_window:
        window[:] = window[-config.loop_window :]
    _maybe_emit_loop_warning(run_id, counts, config, window, emitted)


def record_state(
    state: Any = None,
    meta: dict[str, Any] | None = None,
    diff: Any = None,
) -> None:
    """
    Record a state update event. No-op if no active run (unless AGENTDBG_IMPLICIT_RUN=1).
    Applies redaction and truncation; does not increment any count.
    """
    ctx = _ensure_run()
    if ctx is None:
        return
    run_id, counts, config, window, emitted = ctx
    payload = {"state": state, "diff": diff}
    payload, safe_meta = _apply_redaction_truncation(payload, meta or {}, config)
    ev = new_event(EventType.STATE_UPDATE, run_id, "state", payload, meta=safe_meta)
    _append_event_and_check_guardrails(run_id, ev, config, counts)
    window.append(ev)
    if len(window) > config.loop_window:
        window[:] = window[-config.loop_window :]
    _maybe_emit_loop_warning(run_id, counts, config, window, emitted)
