"""Coordinator thread delivery + agent wake after Kanban lifecycle events."""

from __future__ import annotations

import logging
import threading
import time
from typing import Callable

_log = logging.getLogger(__name__)

GUARD_PREFIX = "[Kanban / recorded, not authoritative]"


def should_reuse_coordinator_delivery_for_idle_wake(history: list) -> bool:
    """True when idle-tick wake should not append another user bubble.

    Delivery already persisted a guard-marked user message; re-persisting
    ``display_text="[Kanban update]"`` duplicates the thread transcript.
    """
    user_checked = 0
    for msg in reversed(history or []):
        if (msg.get("role") or "").strip() != "user":
            continue
        user_checked += 1
        content = msg.get("content")
        if isinstance(content, str) and GUARD_PREFIX in content:
            return True
        if user_checked >= 8:
            break
    return False

_wake_fn: Callable[[str, str], None] | None = None
_chat_reload_fn: Callable[[str], None] | None = None
_wake_lock = threading.Lock()
_last_wake_mono: dict[str, float] = {}
_deferred_wake_timers: dict[str, threading.Timer] = {}
_pending_wake_after_run: dict[str, str] = {}
_pending_wake_fallback_timers: dict[str, threading.Timer] = {}
_WAKE_MIN_INTERVAL_SEC = 30.0
_PENDING_WAKE_TIMEOUT_SEC = 90.0
_WAKE_REASON_PRIORITY: dict[str, int] = {
    "batch_terminal": 3,
    "gave_up": 2,
    "done": 1,
    "kanban_event": 0,
}


def register_coordinator_wake(fn: Callable[[str, str], None]) -> None:
    global _wake_fn
    _wake_fn = fn


def register_coordinator_chat_reload(fn: Callable[[str], None]) -> None:
    global _chat_reload_fn
    _chat_reload_fn = fn


def notify_coordinator_chat_reload(thread_id: str) -> None:
    tid = (thread_id or "").strip()
    if not tid or _chat_reload_fn is None:
        return
    try:
        _chat_reload_fn(tid)
    except Exception:
        _log.debug("coordinator chat reload failed", exc_info=True)


def _cancel_trailing_wake(tid: str) -> None:
    timer = _deferred_wake_timers.pop(tid, None)
    if timer is not None:
        timer.cancel()


def _schedule_trailing_wake(tid: str, reason: str, delay_sec: float) -> None:
    delay = max(0.05, float(delay_sec))

    def _fire() -> None:
        _deferred_wake_timers.pop(tid, None)
        with _wake_lock:
            _last_wake_mono[tid] = time.monotonic()
        _invoke_wake(tid, reason or "kanban_event")

    _cancel_trailing_wake(tid)
    timer = threading.Timer(delay, _fire)
    timer.daemon = True
    _deferred_wake_timers[tid] = timer
    timer.start()


def _invoke_wake(tid: str, reason: str) -> None:
    if _wake_fn is None:
        return
    try:
        _wake_fn(tid, reason or "kanban_event")
    except Exception:
        _log.debug("wake_coordinator_thread failed", exc_info=True)


def _cancel_pending_wake_fallback(tid: str) -> None:
    timer = _pending_wake_fallback_timers.pop(tid, None)
    if timer is not None:
        timer.cancel()


def _schedule_pending_wake_fallback(tid: str) -> None:
    """Fire a queued wake if cleanup never runs (stuck run / missed flush)."""

    def _fire() -> None:
        _pending_wake_fallback_timers.pop(tid, None)
        with _wake_lock:
            reason = _pending_wake_after_run.pop(tid, "")
        if reason:
            _log.info("coordinator pending wake fallback for %s", tid)
            wake_coordinator_thread(tid, reason=reason, force=True)

    _cancel_pending_wake_fallback(tid)
    timer = threading.Timer(_PENDING_WAKE_TIMEOUT_SEC, _fire)
    timer.daemon = True
    _pending_wake_fallback_timers[tid] = timer
    timer.start()


def _merge_pending_wake_reason(prev: str, new: str) -> str:
    prev_v = (prev or "kanban_event").strip() or "kanban_event"
    new_v = (new or "kanban_event").strip() or "kanban_event"
    if _WAKE_REASON_PRIORITY.get(prev_v, 0) >= _WAKE_REASON_PRIORITY.get(new_v, 0):
        return prev_v
    return new_v


def register_pending_coordinator_wake(thread_id: str, *, reason: str = "") -> None:
    """Queue a wake for after the parent thread's current agent run finishes."""
    tid = (thread_id or "").strip()
    if not tid:
        return
    with _wake_lock:
        prev = _pending_wake_after_run.get(tid, "")
        _pending_wake_after_run[tid] = _merge_pending_wake_reason(prev, reason)
    _schedule_pending_wake_fallback(tid)


def flush_pending_coordinator_wake(thread_id: str) -> None:
    """Fire a deferred wake queued while the parent thread was still running."""
    tid = (thread_id or "").strip()
    if not tid:
        return
    _cancel_pending_wake_fallback(tid)
    with _wake_lock:
        reason = _pending_wake_after_run.pop(tid, "")
    if reason:
        wake_coordinator_thread(tid, reason=reason, force=True)


def deliver_coordinator_batch_terminal(
    thread_id: str,
    *,
    batch_id: str,
    tasks: list,
    triggering_task_id: str = "",
) -> bool:
    """Append batch-final summary and wake the coordinator parent thread."""
    tid = (thread_id or "").strip()
    if not tid:
        return False
    from utils.kanban_coordinator import format_batch_terminal_body

    body = format_batch_terminal_body(tasks, triggering_task_id=triggering_task_id)
    banner = f"Kanban batch finished ({batch_id or 'batch'})"
    delivered = deliver_coordinator_continuation(tid, banner=banner, body=body)
    if delivered:
        try:
            from chat_storage import delete_task_plan, ensure_encryption_ready

            ensure_encryption_ready()
            delete_task_plan(tid)
        except Exception:
            _log.debug("clear task plan after batch terminal failed", exc_info=True)
        wake_coordinator_thread(tid, reason="batch_terminal", force=True)
    return delivered


def wake_coordinator_thread(thread_id: str, *, reason: str = "", force: bool = False) -> None:
    tid = (thread_id or "").strip()
    if not tid or _wake_fn is None:
        return
    now = time.monotonic()
    with _wake_lock:
        if not force:
            prev = _last_wake_mono.get(tid)
            if prev is not None and (now - prev) < _WAKE_MIN_INTERVAL_SEC:
                _schedule_trailing_wake(tid, reason, _WAKE_MIN_INTERVAL_SEC - (now - prev))
                return
        _cancel_trailing_wake(tid)
        _last_wake_mono[tid] = now
    _invoke_wake(tid, reason)


def deliver_coordinator_continuation(
    thread_id: str,
    *,
    banner: str,
    body: str,
) -> bool:
    """Append a user-role continuation message (guard-marked, not operator intent).

    Returns True when the message was persisted (caller may wake the parent thread).
    """
    tid = (thread_id or "").strip()
    if not tid:
        return False
    board_active = False
    snap = ""
    try:
        from utils.kanban_coordinator import (
            format_board_status_snapshot,
            probe_board_active,
            tasks_for_active_batch,
        )

        board_active = probe_board_active(tid)
        if board_active:
            snap = format_board_status_snapshot(tasks_for_active_batch(tid))
    except Exception:
        _log.debug("coordinator snapshot for delivery failed", exc_info=True)

    lines = [
        GUARD_PREFIX,
        (banner or "Kanban update").strip(),
        "",
        (body or "").strip(),
        "",
    ]
    if board_active:
        lines.append(
            "Summarize board outcomes for the operator. Do not call implementation tools."
        )
    else:
        lines.append(
            "The Kanban board for this conversation has finished (or has no open tasks). "
            "Summarize final outcomes for the operator. Do not call implementation tools."
        )
    if snap:
        lines.extend(["", "Board snapshot:", snap])
    content = "\n".join(lines).strip()
    try:
        from chat_storage import append_messages

        last_id = append_messages(tid, [{"role": "user", "content": content}])
        if last_id is None:
            return False
        notify_coordinator_chat_reload(tid)
        return True
    except Exception:
        _log.warning("deliver_coordinator_continuation append failed", exc_info=True)
        return False
