"""Council trigger evaluation: tiers, heuristics, phrase overrides, offers."""

from __future__ import annotations

import hashlib
import logging
import re
import uuid
from dataclasses import dataclass
from enum import Enum
from typing import Any

from agent_loop_guardrails import _multi_step_user_message

_log = logging.getLogger(__name__)

COUNCIL_MODES = frozenset({"off", "on_request", "auto_complex", "always"})
COUNCIL_SKIP_LINE = "Answer directly; skip council for this question."

_PHRASE_OVERRIDES = (
    re.compile(r"\bconvene\s+(?:the\s+)?(?:council|experts)\b", re.I),
    re.compile(r"\bdevil'?s\s+advocate\b", re.I),
    re.compile(r"\bstress[- ]?test\b", re.I),
)
_SKIP_PHRASES = (
    re.compile(r"\bjust\s+answer\s+quickly\b", re.I),
    re.compile(r"\bskip\s+council\b", re.I),
    re.compile(r"\bno\s+council\b", re.I),
    re.compile(r"\bdon'?t\s+(?:use\s+)?council\b", re.I),
)
_GREETING_ONLY = re.compile(
    r"^(?:hi|hello|hey|yo|sup|thanks|thank you|ok|okay|sure|yes|no|yep|nope"
    r"|good\s+(?:morning|afternoon|evening|night)|gm|gn)[\s!.?]*$",
    re.I,
)
_STAKES_KEYWORDS = (
    "tradeoff",
    "trade-off",
    "should i",
    "pros and cons",
    "diagnose",
    "architecture",
    "high stakes",
    "high-stakes",
    "compare",
    "evaluate",
    "decision",
    "recommend",
    "risk",
    "verify",
    "validate",
    "proof",
    "evidence",
)


class CouncilTurnAction(str, Enum):
    NONE = "none"
    SKIP = "skip"
    PHRASE_HINT = "phrase_hint"
    OFFER = "offer"
    AUTO_SPAWN = "auto_spawn"
    SPAWN_BLOCKED = "spawn_blocked"


@dataclass(frozen=True)
class CouncilTurnDecision:
    action: CouncilTurnAction
    frame: str = ""
    rationale: str = ""
    message: str = ""


_thread_running_probe: Any = None


def register_thread_running_probe(fn: Any) -> None:
    global _thread_running_probe
    _thread_running_probe = fn


def thread_agent_running(thread_id: str) -> bool:
    tid = (thread_id or "").strip()
    if not tid or _thread_running_probe is None:
        return False
    try:
        return bool(_thread_running_probe(tid))
    except Exception:
        _log.debug("thread running probe failed", exc_info=True)
        return False


def is_council_foreground_allowed(
    policy_context: Any | None,
    *,
    subagent_mode: bool = False,
    is_heartbeat: bool = False,
) -> bool:
    """Mirror User Model / convene_council foreground gate."""
    if subagent_mode or is_heartbeat:
        return False
    if policy_context is None:
        return True
    return not bool(
        getattr(policy_context, "idle_tick", False)
        or getattr(policy_context, "messaging", False)
        or getattr(policy_context, "mesh_sourced", False)
        or getattr(policy_context, "background_automation", False)
        or getattr(policy_context, "scheduled_job", False)
        or getattr(policy_context, "hooks_triggered", False)
        or getattr(policy_context, "unattended_external", False)
        or getattr(policy_context, "subagent_mode", False)
    )


def council_mode() -> str:
    from utils.council_orchestrator import council_settings

    mode = str(council_settings().get("mode") or "off").strip().lower()
    return mode if mode in COUNCIL_MODES else "off"


def council_tool_available() -> bool:
    from utils.council_orchestrator import council_enabled

    return council_enabled() and council_mode() != "off"


def user_requested_skip_council(text: str) -> bool:
    s = (text or "").strip()
    if not s:
        return False
    return any(p.search(s) for p in _SKIP_PHRASES)


def has_council_phrase_override(text: str) -> bool:
    s = (text or "").strip()
    if not s:
        return False
    return any(p.search(s) for p in _PHRASE_OVERRIDES)


def is_greeting_only(text: str) -> bool:
    s = (text or "").strip()
    if not s:
        return True
    if len(s) < 12 and _GREETING_ONLY.match(s):
        return True
    return False


def build_captain_frame(user_text: str) -> str:
    """Normalize the operator question into an immutable council frame."""
    frame = " ".join((user_text or "").split()).strip()
    if len(frame) > 4000:
        frame = frame[:4000].rstrip() + "…"
    return frame


def frame_hash(frame: str) -> str:
    return hashlib.sha256((frame or "").encode("utf-8")).hexdigest()[:16]


def passes_council_minimum_gate(text: str) -> bool:
    """Complexity gate for auto_complex / always (not greeting-only, min length)."""
    s = (text or "").strip()
    if not s or len(s) < 40:
        return False
    if is_greeting_only(s):
        return False
    if user_requested_skip_council(s):
        return False
    lower = s.lower()
    stakes_hits = sum(1 for kw in _STAKES_KEYWORDS if kw in lower)
    if _multi_step_user_message(s):
        return True
    if stakes_hits >= 1 and len(s) >= 60:
        return True
    if s.count("?") >= 2 and len(s) >= 50:
        return True
    return False


def new_offer_id() -> str:
    return f"kcoffer_{uuid.uuid4().hex[:16]}"


def evaluate_council_turn(
    *,
    user_text: str,
    thread_id: str = "",
    in_flight_turn: bool = False,
) -> CouncilTurnDecision:
    """Decide council action for a foreground turn before the model answers."""
    from utils.council_orchestrator import (
        council_batch_overlap_error,
        probe_council_active,
        spawn_preflight_error,
    )

    text = (user_text or "").strip()
    mode = council_mode()

    if not text:
        return CouncilTurnDecision(CouncilTurnAction.NONE)

    if mode == "off":
        if has_council_phrase_override(text):
            return CouncilTurnDecision(
                CouncilTurnAction.SPAWN_BLOCKED,
                message=COUNCIL_OFF_MODE_MESSAGE,
            )
        return CouncilTurnDecision(CouncilTurnAction.NONE)

    if user_requested_skip_council(text):
        return CouncilTurnDecision(CouncilTurnAction.SKIP)

    if probe_council_active(thread_id) or council_batch_overlap_error(origin_thread_id=thread_id):
        return CouncilTurnDecision(CouncilTurnAction.NONE)

    preflight = spawn_preflight_error(
        origin_thread_id=thread_id,
        allow_in_flight_turn=in_flight_turn,
    )
    if preflight:
        # Any spawn preflight refusal (cap, overlap, coordinator, disabled, etc.)
        # must block offers and auto-spawn — otherwise auto_complex shows a broken Approve chip.
        return CouncilTurnDecision(
            CouncilTurnAction.SPAWN_BLOCKED,
            message=preflight,
        )

    frame = build_captain_frame(text)

    if has_council_phrase_override(text):
        if mode == "on_request" or mode in ("auto_complex", "always"):
            return CouncilTurnDecision(
                CouncilTurnAction.PHRASE_HINT,
                frame=frame,
                message=(
                    "The operator invoked a council phrase. Call **convene_council** with a "
                    "clear question_frame, then end the turn without answering substantively."
                ),
            )

    if not passes_council_minimum_gate(text):
        return CouncilTurnDecision(CouncilTurnAction.NONE)

    if mode == "always":
        return CouncilTurnDecision(
            CouncilTurnAction.AUTO_SPAWN,
            frame=frame,
            rationale="Always tier with complexity gate passed.",
        )

    if mode == "auto_complex":
        rationale = (
            "This looks like a multi-step or high-stakes question where a research → "
            "verify → contrarian pipeline could reduce blind spots."
        )
        return CouncilTurnDecision(
            CouncilTurnAction.OFFER,
            frame=frame,
            rationale=rationale,
        )

    return CouncilTurnDecision(CouncilTurnAction.NONE)


COUNCIL_OFFER_PROMPT_TAIL = (
    "\n\n─── Council offer (operator must approve) ───\n"
    "A pending council offer is active for this thread. Briefly explain in one or two sentences "
    "why convening Researcher (Athena) → Verifier (Themis) → Contrarian (Prometheus) might help — then stop.\n"
    "Do NOT answer the substantive question yet. The operator will tap Approve or Skip in chat.\n"
    "Do not call convene_council until they approve.\n"
)

COUNCIL_PHRASE_HINT_TAIL = (
    "\n\n─── Council phrase detected ───\n"
    "The operator asked to convene council. Call **convene_council** with a tight question_frame "
    "capturing their question and constraints, then end this turn without a substantive answer.\n"
)

COUNCIL_OFFER_FAILED_TAIL = (
    "\n\n─── Council offer error ───\n"
    "Council offer storage failed — Approve/Skip chips could not be prepared. "
    "Apologize briefly and ask the operator to resend the question. "
    "Do NOT answer the substantive question in this turn.\n"
)

COUNCIL_OFF_MODE_MESSAGE = (
    "Council mode is off. Enable it in Settings → General → Council mode "
    "(When to convene), or ask me to answer directly."
)

COUNCIL_SPAWN_FAILED_TAIL = (
    "\n\n─── Council could not start ───\n"
    "Council mode could not convene for this turn ({reason}). "
    "Tell the operator clearly (include cap/limit wording when present). "
    "Offer to answer directly or suggest checking Settings → General → Council mode. "
    "Do NOT answer the substantive question in this turn.\n"
)
