"""
sys.setprofile-based auto-instrumentation for custom Python pipelines.

Activated automatically when no known framework (LangGraph / AutoGen / CrewAI)
is detected. Traces every function defined in the user's __main__ script without
any @radar decorator or code changes.
"""
from __future__ import annotations

import sys
import time
import threading
import traceback
import uuid

_call_frames: dict = {}   # frame-id → call metadata
_active = False
_ignored_names = frozenset({
    '<module>', '<lambda>', '<listcomp>', '<dictcomp>',
    '<setcomp>', '<genexpr>', 'main',
})
_dec_codes = None  # lazy ref to decorator._decorated_code_objects (populated on first call)


def start() -> None:
    global _active
    if _active:
        return
    _active = True
    sys.setprofile(_profiler)
    threading.setprofile(_profiler)   # cover new threads too


def stop() -> None:
    global _active
    _active = False
    sys.setprofile(None)
    threading.setprofile(None)


def _profiler(frame, event, arg) -> None:
    # Only instrument functions defined in the user's __main__ script
    if frame.f_globals.get("__name__") != "__main__":
        return

    name = frame.f_code.co_name
    if name.startswith("_") or name in _ignored_names:
        return

    # Skip async coroutine functions entirely — they fire call/return on EVERY
    # await suspension/resumption, which corrupts _last_completed_id timing.
    # Async agents should use @radar; the profiler only auto-instruments sync code.
    if frame.f_code.co_flags & 0x100:  # CO_COROUTINE
        return

    # Avoid double-tracing: skip any function whose code object is registered
    # in _decorated_code_objects (i.e. already wrapped by @trace).
    # This is reliable for both sync and async — unlike the parent-name guard
    # which fails for async because frame.f_back is the event loop, not async_wrapper.
    global _dec_codes
    if _dec_codes is None:
        try:
            from agentradar.instrumentation import decorator as _dec_mod
            _dec_codes = _dec_mod._decorated_code_objects
        except Exception:
            _dec_codes = set()
    if frame.f_code in _dec_codes:
        return

    fid = id(frame)

    if event == "call":
        _on_call(frame, fid, name)
    elif event == "return":
        _on_return(fid, return_value=arg)
    # c_call / c_return / c_exception are for C extensions — skip


def _on_call(frame, fid: int, name: str) -> None:
    try:
        from agentradar.instrumentation.decorator import (
            _ensure_run, _next_step, _state_lock,
        )
        from agentradar.instrumentation import decorator as _dec
        from agentradar.server.events import build_event, emit

        run_id = _ensure_run()
        step   = _next_step()

        with _state_lock:
            parent_id = _dec._last_completed_id
            # If no previous sibling, check if we're nested inside an active
            # traced agent by walking up the call-stack frames.
            if parent_id is None:
                f = frame.f_back
                while f is not None:
                    fid_up = id(f)
                    if fid_up in _call_frames:
                        parent_id = _call_frames[fid_up]["agent_id"]
                        break
                    f = f.f_back
            _dec._active_count += 1

        agent_id = f"{name}_{uuid.uuid4().hex[:8]}"
        captured_input = _capture_args(frame)

        emit(build_event(
            event_type="agent_start",
            run_id=run_id,
            agent_id=agent_id,
            agent_name=name,
            parent_id=parent_id,
            step_number=step,
            input=captured_input,
        ))

        _call_frames[fid] = {
            "agent_id":  agent_id,
            "name":      name,
            "run_id":    run_id,
            "step":      step,
            "parent_id": parent_id,
            "start":     time.monotonic(),
            "input":     captured_input,
        }
    except Exception:
        pass   # must never crash user code


def _on_return(fid: int, return_value) -> None:
    data = _call_frames.pop(fid, None)
    if data is None:
        return

    try:
        duration_ms = (time.monotonic() - data["start"]) * 1000
        from agentradar.server.events import build_event, emit, extract_tokens
        from agentradar.instrumentation import decorator as _dec

        # sys.exc_info() is non-None when an exception is propagating
        exc_info = sys.exc_info()
        is_exc   = exc_info[0] is not None

        if is_exc:
            emit(build_event(
                event_type="agent_error",
                run_id=data["run_id"],
                agent_id=data["agent_id"],
                agent_name=data["name"],
                parent_id=data["parent_id"],
                step_number=data["step"],
                duration_ms=duration_ms,
                error={
                    "message":   str(exc_info[1]),
                    "type":      exc_info[0].__name__,
                    "traceback": "".join(traceback.format_tb(exc_info[2])),
                },
            ))
        else:
            emit(build_event(
                event_type="agent_end",
                run_id=data["run_id"],
                agent_id=data["agent_id"],
                agent_name=data["name"],
                parent_id=data["parent_id"],
                step_number=data["step"],
                duration_ms=duration_ms,
                input=data.get("input"),
                output=return_value,
                tokens=extract_tokens(return_value, data.get("input")),
            ))

        with _dec._state_lock:
            _dec._active_count  -= 1
            _dec._last_completed_id = data["agent_id"]

    except Exception:
        pass


def _capture_args(frame) -> dict | None:
    """Pull the function's argument values from the live frame."""
    try:
        code = frame.f_code
        arg_names = code.co_varnames[: code.co_argcount]
        result = {
            k: frame.f_locals.get(k)
            for k in arg_names
            if k not in ("self", "cls")
        }
        return result or None
    except Exception:
        return None
