"""
Tool-layer logging and status callbacks.
Used by all tool modules.

plugin_stream, tool_stop_check, and live_view use thread-local storage so background
subagent threads get their own callbacks without clobbering the main agent.
"""
import subprocess
import threading
from datetime import datetime
from run_context import get_run_context

_terminal_logger = None
_input_status_callback = None
_chat_event_callback = None

# Thread-local for callbacks that may be set independently per-thread
# (main agent vs background subagents running concurrently).
_tls = threading.local()

# Child Popen handles keyed by ``threading.get_ident()`` so the main thread can
# SIGTERM a worker's children via ``terminate_cancellable_subprocesses_for_thread``
# (tools register on the AgentWorker thread; stop is signaled from the UI thread).
_procs_by_ident: dict[int, set[subprocess.Popen]] = {}
_procs_registry_lock = threading.Lock()


def set_input_status_callback(callback):
    """Set callback for tool progress status (one string arg). Cleared when agent run ends."""
    global _input_status_callback
    _input_status_callback = callback


def _emit_input_status(message):
    """Send a short status line to the input bar if callback is set (no-op otherwise)."""
    if _input_status_callback:
        try:
            _input_status_callback(message)
        except Exception:
            pass


def set_live_view_callback(callback):
    """Set callback for rich Live View events (one dict arg). Cleared when agent run ends.

    Thread-local: each thread can have its own callback (main agent vs concurrent
    kanban workers) without clobbering peers.

    Expected dict shape: {"type": "status"|"progress"|"preview"|"error"|"done",
    "message": str, "progress_pct": int|None, "preview_text": str|None,
    "preview_image_base64": str|None}.
    """
    _tls.live_view_callback = callback


def get_live_view_callback():
    """Return the current thread's live-view callback (or None)."""
    return getattr(_tls, "live_view_callback", None)


def emit_live_view_event(event):
    """Push a rich event to the Live View panel if callback is set (no-op otherwise)."""
    cb = getattr(_tls, "live_view_callback", None)
    if cb:
        try:
            evt = dict(event or {})
            run_ctx = get_run_context()
            run_id = str(evt.get("run_id") or run_ctx.get("run_id") or "").strip()
            if run_id:
                evt["run_id"] = run_id
            cb(evt)
        except Exception:
            pass


def set_chat_event_callback(callback):
    """Set callback for pushing events into the structured chat stream (one dict arg).

    Used by tools (e.g. run_python_code) to inject images or other
    rich content directly into the chat bubble without going through the LLM.
    """
    global _chat_event_callback
    _chat_event_callback = callback


def emit_chat_event(event):
    """Push a structured event into the chat stream if callback is set (no-op otherwise)."""
    if _chat_event_callback:
        try:
            _chat_event_callback(event)
        except Exception:
            pass


def set_plugin_stream_callback(callback):
    """Set callback for realtime plugin stream events (one dict arg).
    Thread-local: each thread can have its own callback."""
    _tls.plugin_stream_callback = callback


def emit_plugin_stream_event(event):
    """Push a structured realtime stream event if callback is set."""
    cb = getattr(_tls, "plugin_stream_callback", None)
    if cb:
        try:
            cb(event)
        except Exception:
            pass


def get_plugin_stream_callback():
    """Return the current thread's plugin stream callback (or None)."""
    return getattr(_tls, "plugin_stream_callback", None)


def set_tool_stop_check_callback(callback):
    """Set callback that returns True when current tool should stop.
    Thread-local: each thread can have its own stop check."""
    _tls.tool_stop_check_callback = callback


def should_stop_tool() -> bool:
    """Return True when the current tool should stop early."""
    cb = getattr(_tls, "tool_stop_check_callback", None)
    if not cb:
        return False
    try:
        return bool(cb())
    except Exception:
        return False


def register_cancellable_subprocess(proc: subprocess.Popen) -> None:
    """Track a child process on the **current** OS thread (typically ``AgentWorker.run``)."""
    ident = threading.get_ident()
    with _procs_registry_lock:
        _procs_by_ident.setdefault(ident, set()).add(proc)


def unregister_cancellable_subprocess(proc: subprocess.Popen) -> None:
    ident = threading.get_ident()
    with _procs_registry_lock:
        bucket = _procs_by_ident.get(ident)
        if bucket is not None:
            bucket.discard(proc)
            if not bucket:
                _procs_by_ident.pop(ident, None)


def _terminate_proc_list(procs: list[subprocess.Popen]) -> int:
    n = 0
    for proc in procs:
        try:
            if proc.poll() is None:
                proc.terminate()
                n += 1
        except Exception:
            pass
    return n


def terminate_cancellable_subprocesses_for_thread(thread_ident: int | None) -> int:
    """SIGTERM children registered on *thread_ident* (e.g. ``AgentWorker.ident`` from the UI thread)."""
    if thread_ident is None:
        return 0
    with _procs_registry_lock:
        procs = list(_procs_by_ident.pop(thread_ident, set()))
    return _terminate_proc_list(procs)


def clear_cancellable_subprocess_registry_for_thread(thread_ident: int | None) -> None:
    """Drop registry entries without signaling (worker shutdown cleanup)."""
    if thread_ident is None:
        return
    with _procs_registry_lock:
        _procs_by_ident.pop(thread_ident, None)


def terminate_cancellable_subprocesses() -> int:
    """SIGTERM children registered on the **current** thread only."""
    return terminate_cancellable_subprocesses_for_thread(threading.get_ident())


def is_tool_realtime(tool_obj) -> bool:
    """Return True if tool_obj has metadata marking it as a real-time / long-running tool."""
    meta = getattr(tool_obj, "metadata", None)
    if isinstance(meta, dict):
        return bool(meta.get("realtime"))
    return False


def set_terminal_logger(logger_callback):
    """Set the terminal logger callback. Callback accepts (message, level)."""
    global _terminal_logger
    _terminal_logger = logger_callback


def _debug_log(message, level="DEBUG"):
    """Log to stdout and terminal window if logger is set. When terminal logger is set, only it is called (no duplicate print)."""
    if level == "DEBUG":
        msg_lower = message.lower()
        if "[error" in msg_lower:
            level = "ERROR"
        elif "[warn" in msg_lower:
            level = "WARN"
        elif "[info" in msg_lower:
            level = "INFO"
        elif "[tool" in msg_lower:
            level = "TOOL"
        elif "[agent" in msg_lower:
            level = "AGENT"
        elif "[pipeline" in msg_lower:
            level = "PIPELINE"
        elif "[trace" in msg_lower:
            level = "TRACE"
    level_tag = {
        "DEBUG": "[DEBUG]", "INFO": "[INFO]", "WARN": "[WARN]", "ERROR": "[ERROR]",
        "TOOL": "[TOOL]", "AGENT": "[AGENT]", "TRACE": "[TRACE]", "PIPELINE": "[PIPELINE]",
    }.get(level, "[INFO]")
    timestamp = datetime.now().strftime("%H:%M:%S")
    msg_stripped = message.strip()
    if level not in {"ERROR", "WARN"}:
        try:
            from core import debug_enabled
            if not debug_enabled():
                return
        except Exception:
            return
    already_has_level = any(
        msg_stripped.startswith(f"[{tag}") for tag in
        ("DEBUG", "INFO", "WARN", "ERROR", "TOOL", "AGENT", "TRACE", "PIPELINE")
    )
    if _terminal_logger:
        try:
            _terminal_logger(message, level)
        except Exception as e:
            print(f"[{timestamp}] [WARN] Terminal logging failed: {e}", flush=True)
        return
    if already_has_level:
        print(f"[{timestamp}] {message}", flush=True)
    else:
        print(f"[{timestamp}] {level_tag} {message}", flush=True)
