"""Evidence-first RevOps chat service for the platform UI."""

from __future__ import annotations

import hashlib
import json
import re
import time
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Callable, Literal
from uuid import uuid4

from pydantic import BaseModel, ConfigDict, Field

from g_gremlin.autopilot.control_plane import (
    ACTION_KEY_REGISTRY,
    DEFAULT_DEDUPE_WINDOW_SECONDS,
    EvidenceV1,
    ProposalV1,
    compile_proposal,
    compute_proposal_signature,
    control_plane_store,
    resolve_deterministic_action_key,
)
from g_gremlin.autopilot.modules import infer_module_write_class_from_tool
from g_gremlin.autopilot.policy import AutonomyPolicy, generate_default_policy
from g_gremlin.dealdesk.context import DealContextV1
from g_gremlin.dealdesk.profiles import DealPatternProfileV1, PatternUpdateEventV1, QuoteEnvironmentProfileV1
from g_gremlin.dealdesk.store import DealDeskStore
from g_gremlin.platform_sfdc_client import run_soql


_SALESFORCE_OPPORTUNITY_ID_RE = re.compile(r"\b006[0-9A-Za-z]{12}(?:[0-9A-Za-z]{3})?\b")
_OPPORTUNITY_TOKEN_RE = re.compile(r"\b(?:opportunity|opp|deal)\s+(?:id\s+)?([A-Za-z0-9._:-]{4,40})\b", re.IGNORECASE)
_STOP_ENTITY_TOKENS = {
    "this",
    "that",
    "there",
    "recent",
    "recently",
    "ready",
    "quote",
    "today",
    "month",
}
_READY_LIFECYCLE_STATES = {"dealdesk_approved", "renewal_shell_created"}
_INTENT_PRIORITY = {
    "reset_conversation": 70,
    "approve_proposal": 60,
    "suggest_action": 55,
    "module_summary": 28,
    "scout_evidence": 25,
    "scout_summary": 22,
    "quote_readiness_overview": 45,
    "quote_readiness": 40,
    "recent_changes_overview": 35,
    "recent_changes": 30,
    "scout": 20,
    "unknown": 0,
}
_QUOTE_READINESS_PHRASES = (
    ("quote-ready", 1.0),
    ("quote ready", 1.0),
    ("quote readiness", 0.95),
    ("ready for quote", 0.9),
    ("ready to quote", 0.9),
    ("quote blocker", 0.88),
    ("quote blockers", 0.88),
    ("quote blocked", 0.88),
    ("blocking quote", 0.84),
    ("quote creation", 0.82),
    ("generate a quote", 0.8),
    ("create a quote", 0.8),
    ("cannot quote", 0.9),
    ("cant quote", 0.9),
    ("can't quote", 0.9),
)
_RECENT_CHANGE_PHRASES = (
    ("what changed", 1.0),
    ("changed on", 0.94),
    ("change history", 0.92),
    ("recent changes", 0.88),
    ("recent deal changes", 0.92),
    ("deal changes", 0.8),
    ("recent history", 0.84),
    ("what happened", 0.86),
    ("recent activity", 0.82),
    ("activity history", 0.82),
)
_DNC_PHRASES = (
    ("dnc", 1.0),
    ("do not contact", 1.0),
    ("suppression", 0.88),
    ("suppressed", 0.82),
    ("unsubscribe violation", 0.82),
    ("opt out violation", 0.82),
)
_RENEWAL_PHRASES = (
    ("renewal", 0.72),
    ("renewals", 0.72),
    ("expiring", 0.62),
)
_NO_ACTIVITY_PHRASES = (
    ("no activity", 0.92),
    ("inactive", 0.86),
    ("no touch", 0.82),
    ("no touches", 0.82),
    ("zero activity", 0.88),
    ("stalled", 0.76),
    ("no recent activity", 0.9),
)
_SCOUT_EVIDENCE_PHRASES = (
    ("show the evidence", 1.0),
    ("show evidence", 0.96),
    ("sample evidence", 0.9),
    ("show the records", 0.94),
    ("which records", 0.94),
    ("which record", 0.9),
    ("record ids", 0.9),
    ("affected records", 0.9),
    ("who is affected", 0.88),
    ("show me the affected records", 0.98),
)
_SCOUT_SUMMARY_PHRASES = (
    ("which scouts found issues", 1.0),
    ("which scouts found findings", 0.98),
    ("which scouts found problems", 0.96),
    ("which scouts had issues", 0.95),
    ("which scouts had findings", 0.94),
    ("scout summary", 0.76),
    ("scout activity", 0.68),
)
_PAUSE_SEQUENCE_ACTION_PHRASES = (
    ("pause that sequence", 1.0),
    ("pause this sequence", 0.98),
    ("pause the sequence", 0.98),
    ("pause sequence", 0.94),
    ("draft a pause sequence proposal", 0.9),
    ("create a pause sequence proposal", 0.88),
    ("suggest a pause sequence action", 0.86),
)
_FOLLOW_UP_TASK_ACTION_PHRASES = (
    ("create a follow-up task", 1.0),
    ("create follow-up task", 0.98),
    ("create follow up task", 0.98),
    ("draft a follow-up task", 0.94),
    ("draft follow-up task", 0.92),
    ("make a follow-up task", 0.9),
)
_ROUTE_EXCEPTION_ACTION_PHRASES = (
    ("route this to slack", 1.0),
    ("route that to slack", 0.98),
    ("route the exception", 0.96),
    ("route this exception", 0.96),
    ("send this to slack", 0.94),
    ("send that to slack", 0.92),
    ("slack this exception", 0.9),
)
_MODULE_SUMMARY_REQUEST_PHRASES = (
    ("what did", 0.42),
    ("summarize", 0.38),
    ("summary", 0.34),
    ("activity", 0.28),
)
_MODULE_SUMMARY_TARGET_PHRASES: dict[str, tuple[tuple[str, float], ...]] = {
    "sequence_pause": (
        ("auto-pause", 1.0),
        ("auto pause", 1.0),
        ("sequence pause", 0.94),
        ("sequence_pause", 0.94),
        ("pause sequence", 0.9),
    ),
}
_MODULE_SUMMARY_DISPLAY_NAMES = {
    "sequence_pause": "auto-pause",
}
_SUMMARY_WINDOW_FOLLOW_UP_PHRASES = (
    "today",
    "this week",
    "this month",
    "last week",
    "last 24 hours",
    "last 7 days",
    "past 7 days",
)
_SUMMARY_FOLLOW_UP_HINTS = (
    "what about",
    "how about",
    "what did it do",
    "what did they do",
    "same window",
    "same period",
)
_RESET_CONVERSATION_PHRASES = (
    ("start a new investigation", 1.0),
    ("new investigation", 0.94),
    ("start over", 0.9),
    ("reset this investigation", 0.92),
    ("reset investigation", 0.9),
    ("clear this investigation", 0.86),
    ("start a new thread", 0.84),
)
_APPROVAL_REQUEST_PHRASES = (
    ("approve it", 1.0),
    ("approve this", 0.96),
    ("approve the proposal", 0.96),
)
_FOLLOW_UP_BREACH_PHRASES = (
    ("meeting follow-up breach", 1.0),
    ("meeting follow up breach", 1.0),
    ("follow-up breach", 0.94),
    ("follow up breach", 0.94),
    ("missed follow-up", 0.9),
    ("missed follow up", 0.9),
    ("overdue follow-up", 0.88),
    ("overdue follow up", 0.88),
)
_FIELD_DRIFT_PHRASES = (
    ("field drift", 1.0),
    ("cross-system field drift", 1.0),
    ("cross system field drift", 1.0),
    ("cross-system drift", 0.94),
    ("cross system drift", 0.94),
)
_CUSTOMER_GUARDRAIL_PHRASES = (
    ("customer guardrail", 1.0),
    ("open ticket guardrail", 0.98),
    ("customer open ticket", 0.94),
    ("open support ticket", 0.9),
    ("ticket in sequence", 0.9),
)
_SINGULAR_DEAL_REFERENCES = (
    "this opportunity",
    "this deal",
    "that opportunity",
    "that deal",
)
_OVERVIEW_REQUEST_PHRASES = (
    "which opportunities",
    "which deals",
    "all opportunities",
    "all deals",
    "queue",
    "right now",
    "overview",
    "summary",
)
_RECENT_CHANGE_FOLLOW_UP_HINTS = (
    "what changed",
    "change history",
    "recent changes",
    "recent history",
    "what happened",
    "recently",
    "activity history",
)
_QUOTE_READINESS_FOLLOW_UP_HINTS = (
    "quote-ready",
    "quote ready",
    "quote blockers",
    "quote blocker",
    "blocking quote",
    "ready to quote",
    "ready for quote",
    "can't quote",
    "cannot quote",
)
_SHORT_QUOTE_READINESS_FOLLOW_UPS = {
    "why",
    "why?",
    "why not",
    "why not?",
    "blockers",
    "blockers?",
}
_SCOUT_EVIDENCE_FOLLOW_UP_HINTS = (
    "show the evidence",
    "show evidence",
    "sample evidence",
    "show the records",
    "which records",
    "which record",
    "record ids",
    "affected records",
    "who is affected",
)
_SEVERITY_RANKS = {
    "critical": 4,
    "high": 3,
    "medium": 2,
    "low": 1,
    "info": 0,
    "unknown": 0,
}
_SOURCE_TYPE_RANKS = {
    "dealdesk_context": 220,
    "salesforce_query": 210,
    "receipt": 180,
    "pattern_update_event": 170,
    "scout_finding": 165,
    "scout_run": 155,
    "dealdesk_pattern_profile": 145,
    "dealdesk_environment_profile": 140,
}
_SUCCESS_RECEIPT_OUTCOMES = {"success", "approved", "completed", "ok"}
_APPROVAL_SURFACE_PATH = "/exceptions"
_DEFAULT_CHAT_PROPOSAL_ACTOR = "ai:revops-chat"
_GROUND_CHAT_ACTION_SCOUTS: dict[str, set[str]] = {
    "sequence_pause": {"do_not_contact"},
    "follow_up_enforcement": {"meeting_followup_breach"},
    "slack_routing": {"cross_system_field_drift", "customer_open_ticket_guardrail"},
}
_SLACK_ROUTING_DEFAULT_CHANNEL = "#revops-alerts"
_SLACK_ROUTING_DEFAULT_SEVERITY = "high"
_SLACK_ROUTING_TITLE_PREFIX = "Autopilot Exception"


class RevOpsChatRequest(BaseModel):
    model_config = ConfigDict(extra="ignore")

    message: str = Field(min_length=1, max_length=4000)
    org_alias: str | None = None
    conversation_id: str | None = None


class RevOpsChatSource(BaseModel):
    model_config = ConfigDict(extra="ignore")

    source_id: str
    source_type: Literal[
        "scout_finding",
        "scout_run",
        "dealdesk_context",
        "dealdesk_pattern_profile",
        "dealdesk_environment_profile",
        "pattern_update_event",
        "receipt",
        "salesforce_query",
    ]
    title: str
    summary: str
    captured_at: str | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)


class RevOpsBudgetUsed(BaseModel):
    model_config = ConfigDict(extra="ignore")

    live_salesforce_queries: int = 0
    live_salesforce_rows: int = 0
    live_salesforce_time_ms: int = 0
    max_live_salesforce_queries: int = 1
    max_live_salesforce_rows: int = 25
    max_live_salesforce_time_ms: int = 1500
    fallback_used: bool = False
    budget_exceeded: bool = False
    notes: list[str] = Field(default_factory=list)


class RevOpsChatProposalPayload(BaseModel):
    model_config = ConfigDict(extra="ignore")

    proposal_id: str
    action_key: str
    risk_tier: str
    approval_path: str
    pending_run_id: str | None = None
    compile_id: str | None = None
    source_kind: str | None = None
    action_summary: str | None = None
    approval_required: bool = True


class RevOpsChatResponse(BaseModel):
    model_config = ConfigDict(extra="ignore")

    status: Literal["grounded", "insufficient_evidence"]
    answer: str
    confidence: float = Field(ge=0.0, le=1.0)
    sources: list[RevOpsChatSource] = Field(default_factory=list)
    tools_used: list[str] = Field(default_factory=list)
    budget_used: RevOpsBudgetUsed = Field(default_factory=RevOpsBudgetUsed)
    follow_up_suggestions: list[str] = Field(default_factory=list)
    conversation_id: str | None = None
    proposal: RevOpsChatProposalPayload | None = None


@dataclass(frozen=True)
class _QuestionIntent:
    intent: Literal[
        "reset_conversation",
        "approve_proposal",
        "module_summary",
        "scout_evidence",
        "scout_summary",
        "quote_readiness",
        "quote_readiness_overview",
        "recent_changes",
        "recent_changes_overview",
        "scout",
        "suggest_action",
        "unknown",
    ]
    scout_name: str | None = None
    opportunity_id: str | None = None
    action_key: str | None = None
    module_id: str | None = None
    summary_window_label: str | None = None
    matched_signals: tuple[str, ...] = ()
    score: float = 0.0


@dataclass(frozen=True)
class _SummaryWindow:
    label: str
    start_at: datetime
    end_at: datetime | None = None


@dataclass(frozen=True)
class _ConversationScope:
    conversation_id: str
    last_intent: str | None = None
    last_scout_name: str | None = None
    last_opportunity_id: str | None = None
    last_module_id: str | None = None
    last_org_alias: str | None = None
    last_summary_intent: str | None = None
    last_summary_window: str | None = None
    last_summary_org_alias: str | None = None
    last_grounded_scout_name: str | None = None
    last_grounded_run_id: str | None = None
    last_grounded_finding_index: int | None = None
    last_response_sources: tuple[RevOpsChatSource, ...] = ()
    last_proposal_id: str | None = None
    last_proposal_action_key: str | None = None
    last_proposal_risk_tier: str | None = None
    last_proposal_pending_run_id: str | None = None
    last_proposal_approval_path: str | None = None


class RevOpsChatService:
    """Deterministic Q&A over local RevOps evidence with narrow CRM fallback."""

    def __init__(
        self,
        state_dir: Path,
        *,
        soql_runner: Callable[..., list[dict[str, str]]] | None = None,
        policy: AutonomyPolicy | None = None,
    ) -> None:
        self.state_dir = Path(state_dir).resolve()
        self.store = DealDeskStore(self.state_dir)
        self._soql_runner = soql_runner or run_soql
        self._policy = policy or generate_default_policy("default")
        self._control_plane = control_plane_store(self.state_dir)

    def answer(self, request: RevOpsChatRequest) -> RevOpsChatResponse:
        question = str(request.message or "").strip()
        conversation = self._resolve_conversation_scope(request.conversation_id)
        requested_org_alias = str(request.org_alias or "").strip() or None
        resolved_org_alias = requested_org_alias or (str(conversation.last_org_alias or "").strip() or None)
        if not question:
            result = self._insufficient_evidence(
                answer="Ask a concrete operational question to start an evidence-backed investigation.",
                confidence=0.0,
                follow_ups=self._default_follow_ups(),
            )
            return self._finalize_response(
                question=question,
                parsed=_QuestionIntent(intent="unknown"),
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )

        parsed = self._classify_question(question)
        parsed = self._hydrate_intent_from_conversation(parsed, question=question, conversation=conversation)
        if parsed.intent == "reset_conversation":
            fresh_conversation = _ConversationScope(conversation_id=f"revops-{uuid4().hex[:12]}")
            result = self._answer_reset_conversation()
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=fresh_conversation,
                org_alias=requested_org_alias,
            )
        if parsed.intent == "approve_proposal":
            result = self._answer_approval_request(conversation=conversation)
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "suggest_action":
            result = self._answer_suggest_action(
                parsed=parsed,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "scout_summary":
            result = self._answer_scout_summary(
                question=question,
                org_alias=resolved_org_alias,
                window_label=parsed.summary_window_label,
            )
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "scout" and parsed.scout_name:
            result = self._answer_scout_question(parsed.scout_name)
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "scout_evidence" and parsed.scout_name:
            result = self._answer_scout_evidence(parsed.scout_name)
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "module_summary" and parsed.module_id:
            result = self._answer_module_summary(
                module_id=parsed.module_id,
                question=question,
                org_alias=resolved_org_alias,
                window_label=parsed.summary_window_label,
            )
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "quote_readiness":
            result = self._answer_quote_readiness(
                opportunity_id=parsed.opportunity_id,
                org_alias=resolved_org_alias,
            )
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "quote_readiness_overview":
            result = self._answer_quote_readiness_overview(org_alias=resolved_org_alias)
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "recent_changes":
            result = self._answer_recent_changes(
                opportunity_id=parsed.opportunity_id,
                org_alias=resolved_org_alias,
            )
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        if parsed.intent == "recent_changes_overview":
            result = self._answer_recent_changes_overview(org_alias=resolved_org_alias)
            return self._finalize_response(
                question=question,
                parsed=parsed,
                response=result,
                conversation=conversation,
                org_alias=resolved_org_alias,
            )
        result = self._insufficient_evidence(
            answer=(
                "This slice answers evidence-backed questions about quote readiness, recent deal changes, renewals due "
                "with no activity, DNC violations, meeting follow-up breaches, field drift, customer guardrails, "
                "scout activity summaries, and auto-pause receipt summaries. It can also draft grounded action "
                "proposals for supported action families."
            ),
            confidence=0.08,
            follow_ups=self._default_follow_ups(),
        )
        return self._finalize_response(
            question=question,
            parsed=parsed,
            response=result,
            conversation=conversation,
            org_alias=resolved_org_alias,
        )

    def _answer_scout_question(self, scout_name: str) -> RevOpsChatResponse:
        latest = self._latest_scan_finding_for_scout(scout_name)
        if latest is None:
            return self._insufficient_evidence(
                answer=f"No stored scout finding is available yet for `{scout_name}`.",
                confidence=0.18,
                tools_used=["scout.findings.query"],
                follow_ups=[
                    "Ask for a specific opportunity ID if you need deal-level evidence.",
                    "Open Signals to confirm whether this scout has run recently.",
                ],
            )

        scan_payload, finding, finding_index = latest
        findings_count = _coerce_int(finding.get("findings_count"), 0)
        records_scanned = _coerce_int(finding.get("total_records_scanned"), 0)
        status = str(finding.get("status") or "unknown").strip().lower() or "unknown"
        severity = str(finding.get("severity") or "").strip().lower() or "medium"
        evidence_preview = self._scout_evidence_preview(finding)
        display_name = _humanize_token(scout_name)
        ended_at = _captured_at(scan_payload)

        if findings_count <= 0 or status == "ok":
            answer = (
                f"The latest `{display_name}` scout run did not surface active findings. "
                f"It scanned {records_scanned} record(s){_at_time_suffix(ended_at)}."
            )
            confidence = 0.91
        else:
            answer = (
                f"The latest `{display_name}` scout run found {findings_count} item(s) across {records_scanned} scanned record(s)"
                f"{_at_time_suffix(ended_at)}."
            )
            if evidence_preview:
                answer += f" Sample evidence: {evidence_preview}."
            confidence = 0.93

        sources = [
            RevOpsChatSource(
                source_id=f"{scan_payload.get('run_id') or 'scan'}:{scout_name}",
                source_type="scout_finding",
                title=f"{display_name} finding snapshot",
                summary=f"{findings_count} finding(s), status `{status}`, severity `{severity}`.",
                captured_at=ended_at,
                metadata={
                    "scout_name": scout_name,
                    "run_id": scan_payload.get("run_id"),
                    "finding_index": finding_index,
                    "status": status,
                    "severity": severity,
                    "findings_count": findings_count,
                    "records_scanned": records_scanned,
                },
            ),
            RevOpsChatSource(
                source_id=str(scan_payload.get("run_id") or scout_name),
                source_type="scout_run",
                title=f"Scout run {scan_payload.get('run_id') or 'unknown'}",
                summary="Latest stored scan run for this signal.",
                captured_at=ended_at,
                metadata={
                    "run_id": scan_payload.get("run_id"),
                    "finding_index": finding_index,
                    "started_at": scan_payload.get("started_at"),
                    "ended_at": scan_payload.get("ended_at"),
                    "errors_count": len(scan_payload.get("errors") or []) if isinstance(scan_payload.get("errors"), list) else 0,
                },
            ),
        ]
        return RevOpsChatResponse(
            status="grounded",
            answer=answer,
            confidence=confidence,
            sources=sources,
            tools_used=["scout.findings.query"],
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "Open Signals for the full evidence list and scout history.",
                "Ask what changed on a specific deal if you need receipt-level context.",
            ],
        )

    def _answer_scout_evidence(self, scout_name: str) -> RevOpsChatResponse:
        latest = self._latest_scan_finding_for_scout(scout_name)
        if latest is None:
            return self._insufficient_evidence(
                answer=f"No stored scout finding is available yet for `{scout_name}`.",
                confidence=0.18,
                tools_used=["scout.findings.query"],
                follow_ups=[
                    "Show me recent DNC violations.",
                    "Which renewals are due this month with no activity?",
                ],
            )

        scan_payload, finding, finding_index = latest
        evidence_rows = finding.get("evidence") if isinstance(finding.get("evidence"), list) else []
        display_name = _humanize_token(scout_name)
        ended_at = _captured_at(scan_payload)
        findings_count = _coerce_int(finding.get("findings_count"), 0)
        if not evidence_rows:
            return self._insufficient_evidence(
                answer=(
                    f"The latest `{display_name}` scout run reports {findings_count} finding(s){_at_time_suffix(ended_at)}, "
                    "but the stored scan payload does not include row-level evidence."
                ),
                confidence=0.42,
                sources=[
                    RevOpsChatSource(
                        source_id=f"{scan_payload.get('run_id') or 'scan'}:{scout_name}",
                        source_type="scout_finding",
                        title=f"{display_name} finding snapshot",
                        summary="Latest stored scout finding did not carry row-level evidence.",
                        captured_at=ended_at,
                        metadata={
                            "scout_name": scout_name,
                            "run_id": scan_payload.get("run_id"),
                            "finding_index": finding_index,
                            "findings_count": findings_count,
                            "records_scanned": _coerce_int(finding.get("total_records_scanned"), 0),
                        },
                    )
                ],
                tools_used=["scout.findings.query"],
                follow_ups=[
                    "Open Signals to inspect the full scout artifact.",
                    "Ask for the latest scout summary if you only need counts and severity.",
                ],
            )

        evidence_preview: list[str] = []
        for raw in evidence_rows[:5]:
            if not isinstance(raw, dict):
                continue
            record_id = str(raw.get("record_id") or raw.get("id") or "unknown").strip() or "unknown"
            reason = str(raw.get("reason") or raw.get("reason_code") or "").strip()
            if reason:
                evidence_preview.append(f"`{record_id}` ({reason})")
            else:
                evidence_preview.append(f"`{record_id}`")
        preview_text = "; ".join(evidence_preview) if evidence_preview else "No displayable evidence rows were stored."
        answer = (
            f"The latest `{display_name}` scout run captured {len(evidence_rows)} row-level evidence item(s)"
            f"{_at_time_suffix(ended_at)}. {preview_text}."
        )
        if len(evidence_rows) > len(evidence_preview):
            answer += f" Showing {len(evidence_preview)} of {len(evidence_rows)} stored row(s)."

        sources = [
            RevOpsChatSource(
                source_id=f"{scan_payload.get('run_id') or 'scan'}:{scout_name}",
                source_type="scout_finding",
                title=f"{display_name} evidence rows",
                summary=f"{len(evidence_rows)} stored row-level evidence item(s) from the latest scout finding.",
                captured_at=ended_at,
                metadata={
                    "scout_name": scout_name,
                    "run_id": scan_payload.get("run_id"),
                    "finding_index": finding_index,
                    "findings_count": findings_count,
                    "records_scanned": _coerce_int(finding.get("total_records_scanned"), 0),
                    "evidence_preview": evidence_preview,
                },
            ),
            RevOpsChatSource(
                source_id=str(scan_payload.get("run_id") or scout_name),
                source_type="scout_run",
                title=f"Scout run {scan_payload.get('run_id') or 'unknown'}",
                summary="Latest stored scan run backing the row-level evidence preview.",
                captured_at=ended_at,
                metadata={
                    "run_id": scan_payload.get("run_id"),
                    "finding_index": finding_index,
                    "started_at": scan_payload.get("started_at"),
                    "ended_at": scan_payload.get("ended_at"),
                    "errors_count": len(scan_payload.get("errors") or []) if isinstance(scan_payload.get("errors"), list) else 0,
                },
            ),
        ]
        return RevOpsChatResponse(
            status="grounded",
            answer=answer,
            confidence=0.94,
            sources=sources,
            tools_used=["scout.findings.query"],
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "Ask for the latest scout summary if you want severity and finding counts.",
                "Open Signals for the full evidence table and scan history.",
            ],
        )

    def _answer_suggest_action(
        self,
        *,
        parsed: _QuestionIntent,
        conversation: _ConversationScope,
        org_alias: str | None,
    ) -> RevOpsChatResponse:
        action_key = str(parsed.action_key or "").strip()
        if action_key not in _GROUND_CHAT_ACTION_SCOUTS:
            return self._insufficient_evidence(
                answer="This chat slice only drafts deterministic grounded proposals for supported action families.",
                confidence=0.12,
                follow_ups=self._action_follow_ups(action_key),
            )

        action_context = self._load_grounded_action_context(
            conversation=conversation,
            org_alias=org_alias,
            action_key=action_key,
        )
        if action_context is None:
            return self._insufficient_evidence(
                answer=self._grounded_action_requirement_message(action_key),
                confidence=0.18,
                follow_ups=self._action_follow_ups(action_key),
            )

        scan_payload, finding, finding_index, scoped_rows = action_context
        draft = self._build_grounded_action_proposal(
            action_key=action_key,
            conversation=conversation,
            org_alias=org_alias,
            scan_payload=scan_payload,
            finding=finding,
            finding_index=finding_index,
            scoped_rows=scoped_rows,
        )
        if draft is None:
            return self._insufficient_evidence(
                answer=self._grounded_action_target_message(action_key),
                confidence=0.24,
                sources=self._build_scout_sources(scan_payload, finding, finding_index),
                tools_used=["scout.findings.query"],
                follow_ups=self._action_follow_ups(action_key),
            )

        proposal, risk_profile, action_summary, answer_text, response_confidence = draft
        saved, was_deduped = self._control_plane.save_proposal(proposal)
        if not was_deduped:
            self._control_plane.record_outcome(
                saved.proposal_id,
                "proposed",
                actor=_DEFAULT_CHAT_PROPOSAL_ACTOR,
                detail={
                    "source": "revops_chat",
                    "conversation_id": conversation.conversation_id,
                    "run_id": scan_payload.get("run_id"),
                    "finding_index": finding_index,
                },
            )
            self._control_plane.save_evidence(
                self._build_scout_evidence_rows(
                    proposal_id=saved.proposal_id,
                    evidence_ids=list(saved.evidence_ids),
                    scan_payload=scan_payload,
                    finding_index=finding_index,
                    scout_name=str(finding.get("scout_name") or "").strip() or "unknown",
                    rows=scoped_rows,
                )
            )

        compile_result = self._control_plane.latest_compile_result(saved.proposal_id)
        pending_run_id: str | None = compile_result.plan_id if compile_result is not None else None
        compile_id: str | None = compile_result.compile_id if compile_result is not None else None
        if compile_result is None:
            compile_result, plan = compile_proposal(
                store=self._control_plane,
                state_dir=self.state_dir,
                policy=self._policy,
                proposal_id=saved.proposal_id,
                emit_pending=True,
                actor=_DEFAULT_CHAT_PROPOSAL_ACTOR,
            )
            pending_run_id = plan.run_id if plan is not None else compile_result.plan_id
            compile_id = compile_result.compile_id

        return RevOpsChatResponse(
            status="grounded",
            answer=answer_text,
            confidence=response_confidence,
            sources=self._build_scout_sources(scan_payload, finding, finding_index),
            tools_used=["scout.findings.query", "control_plane.proposal.save", "control_plane.proposal.compile"],
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "Open this proposal in Approvals.",
                "Show me the risk factors.",
                "Why is this action recommended?",
            ],
            proposal=RevOpsChatProposalPayload(
                proposal_id=saved.proposal_id,
                action_key=action_key,
                risk_tier=str(risk_profile.get("tier") or "low"),
                approval_path=_APPROVAL_SURFACE_PATH,
                pending_run_id=pending_run_id,
                compile_id=compile_id,
                source_kind=saved.source_kind,
                action_summary=action_summary,
                approval_required=True,
            ),
        )

    def _answer_reset_conversation(self) -> RevOpsChatResponse:
        return self._insufficient_evidence(
            answer=(
                "Started a new investigation thread. Previous carry-forward context will not be reused in this "
                "conversation."
            ),
            confidence=0.95,
            tools_used=["conversation.reset"],
            follow_ups=self._default_follow_ups(),
        )

    def _answer_approval_request(self, *, conversation: _ConversationScope) -> RevOpsChatResponse:
        proposal_id = str(conversation.last_proposal_id or "").strip()
        approval_path = str(conversation.last_proposal_approval_path or _APPROVAL_SURFACE_PATH).strip() or _APPROVAL_SURFACE_PATH
        if not proposal_id:
            return self._insufficient_evidence(
                answer="There is no drafted proposal in this conversation to route for approval yet.",
                confidence=0.12,
                follow_ups=[
                    "Show me recent DNC violations.",
                    "Pause that sequence.",
                ],
            )

        action_key = str(conversation.last_proposal_action_key or "sequence_pause").strip() or "sequence_pause"
        risk_tier = str(conversation.last_proposal_risk_tier or "low").strip() or "low"
        pending_run_id = str(conversation.last_proposal_pending_run_id or "").strip() or None
        return RevOpsChatResponse(
            status="grounded",
            answer=(
                "Chat cannot approve or execute proposals. "
                f"Open Pending Approvals at `{approval_path}`"
                f"{f' and review run `{pending_run_id}`' if pending_run_id else ''}."
            ),
            confidence=0.9,
            sources=list(conversation.last_response_sources),
            tools_used=["control_plane.proposal.lookup"],
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "Open this proposal in Approvals.",
                "Why is this action recommended?",
            ],
            proposal=RevOpsChatProposalPayload(
                proposal_id=proposal_id,
                action_key=action_key,
                risk_tier=risk_tier,
                approval_path=approval_path,
                pending_run_id=pending_run_id,
                source_kind="hybrid",
                approval_required=True,
            ),
        )

    def _answer_quote_readiness(
        self,
        *,
        opportunity_id: str | None,
        org_alias: str | None,
    ) -> RevOpsChatResponse:
        if not opportunity_id:
            return self._insufficient_evidence(
                answer="Include a specific opportunity ID to investigate quote readiness from Deal Desk evidence.",
                confidence=0.12,
                follow_ups=[
                    "Why is opportunity 006XXXXXXXXXXXXXXX not quote-ready?",
                    "What changed on deal 006XXXXXXXXXXXXXXX recently?",
                ],
            )

        budget = RevOpsBudgetUsed()
        context = self._load_context_for_opportunity(opportunity_id, org_alias=org_alias)
        if context is None:
            salesforce_source, snapshot = self._lookup_opportunity_snapshot(
                opportunity_id=opportunity_id,
                org_alias=org_alias,
                budget=budget,
            )
            sources = [salesforce_source] if salesforce_source is not None else []
            tools_used = ["sfdc.soql.read"] if salesforce_source is not None else []
            stage_name = str(snapshot.get("StageName") or "unknown").strip() if snapshot else "unknown"
            return self._insufficient_evidence(
                answer=(
                    f"I could not explain quote readiness for opportunity `{opportunity_id}` from stored Deal Desk evidence. "
                    f"{'A single live Salesforce read found the opportunity at stage `' + stage_name + '`, but there is still no local Deal Desk context or readiness artifact to ground a diagnosis.' if snapshot else 'No local Deal Desk context or guarded Salesforce fallback result is available.'}"
                ),
                confidence=0.34 if snapshot else 0.14,
                sources=sources,
                tools_used=tools_used,
                budget_used=budget,
                follow_ups=[
                    "Open the Deal Desk module probe outside chat if you need to generate a fresh readiness context.",
                    "Ask what changed on this deal recently once receipts or pattern events exist.",
                ],
            )

        pattern_profile = self.store.load_deal_pattern_profile(
            crm_org_id=context.crm_org_id,
            org_alias=context.org_alias,
        )
        environment_profile = self.store.load_quote_environment_profile(
            crm_org_id=context.crm_org_id,
            org_alias=context.org_alias,
        )
        receipts = self._load_related_receipts(
            opportunity_id=opportunity_id,
            receipt_ids=context.receipt_ids,
            org_alias=context.org_alias or org_alias,
        )
        events = self._load_related_pattern_events(
            opportunity_id=opportunity_id,
            crm_org_id=context.crm_org_id,
            org_alias=context.org_alias,
        )

        ready, blockers, close_findings = self._context_readiness_snapshot(context)
        latest_receipt = receipts[0] if receipts else None
        latest_event = events[0] if events else None

        answer_parts = []
        if ready:
            answer_parts.append(
                f"Stored Deal Desk evidence shows opportunity `{opportunity_id}` as quote-ready."
            )
        else:
            answer_parts.append(
                f"Stored Deal Desk evidence does not show opportunity `{opportunity_id}` as quote-ready."
            )
        answer_parts.append(f"Lifecycle state is `{context.lifecycle_state}`.")
        if blockers:
            answer_parts.append(f"Open assumptions: {', '.join(blockers[:3])}.")
        if close_findings:
            answer_parts.append(f"Recorded close findings: {', '.join(close_findings[:3])}.")
        if pattern_profile is not None:
            answer_parts.append(
                "Pattern profile "
                f"`{pattern_profile.learning_state}` spans {pattern_profile.source_record_count} historical deal(s)."
            )
        if environment_profile is not None and environment_profile.detected_engine != "unknown":
            answer_parts.append(
                f"Detected quote engine is `{environment_profile.detected_engine}` "
                f"(confidence {environment_profile.engine_confidence:.2f})."
            )
        conflict_notes = self._quote_readiness_conflict_notes(
            context=context,
            ready=ready,
            latest_receipt=latest_receipt,
            latest_event=latest_event,
        )
        if conflict_notes:
            answer_parts.append(f"Potential evidence conflict: {' '.join(conflict_notes[:2])}")
        answer = " ".join(answer_parts)

        context_source = self._context_source(context)
        receipt_sources = [self._receipt_source(payload) for payload in receipts[:2]]
        event_sources = [self._pattern_event_source(event) for event in events[:2]]
        if latest_receipt is not None and receipt_sources:
            latest_outcome = str(latest_receipt.get("outcome") or "unknown").strip().lower() or "unknown"
            latest_receipt_ts = _sort_timestamp(_captured_at(latest_receipt))
            context_ts = _sort_timestamp(context.updated_at or context.created_at)
            if ready and latest_outcome not in _SUCCESS_RECEIPT_OUTCOMES:
                receipt_sources[0].metadata = {
                    "conflict_note": (
                        f"Latest receipt outcome `{latest_outcome}` conflicts with the stored ready lifecycle state."
                    ),
                    **receipt_sources[0].metadata,
                }
            elif latest_receipt_ts > context_ts:
                context_source.metadata = {
                    "staleness_note": "Newer receipt evidence exists after this context snapshot.",
                    **context_source.metadata,
                }
        if latest_event is not None and event_sources:
            current_motion = str(context.inferred_motion_id or "").strip()
            latest_motion = str(latest_event.extracted_motion_id or "").strip()
            if current_motion and latest_motion and current_motion != latest_motion:
                event_sources[0].metadata = {
                    "conflict_note": (
                        f"Pattern event motion `{latest_motion}` disagrees with stored motion `{current_motion}`."
                    ),
                    **event_sources[0].metadata,
                }
        sources = [
            context_source,
            *([self._pattern_profile_source(pattern_profile)] if pattern_profile is not None else []),
            *([self._environment_profile_source(environment_profile)] if environment_profile is not None else []),
            *receipt_sources,
            *event_sources,
        ]
        confidence = _clamp_confidence(
            context.confidence_at_creation
            or context.inferred_motion_confidence
            or (pattern_profile.profile_confidence if pattern_profile is not None else 0.62),
            floor=0.42,
        )
        if ready:
            confidence = max(confidence, 0.82)
        else:
            confidence = max(confidence, 0.76)
        if conflict_notes:
            confidence = min(confidence, 0.68 if ready else 0.72)

        tools_used = ["dealdesk.context.query"]
        if receipts or events:
            tools_used.append("receipts.query")
        return RevOpsChatResponse(
            status="grounded",
            answer=answer,
            confidence=confidence,
            sources=sources,
            tools_used=_unique_strings(tools_used),
            budget_used=budget,
            follow_up_suggestions=[
                "Ask what changed on this deal recently for event and receipt history.",
                "Open the Deal Desk setup flow if you need to compare this deal to historical exemplars.",
            ],
        )

    def _answer_quote_readiness_overview(self, *, org_alias: str | None) -> RevOpsChatResponse:
        contexts = self._load_all_contexts(org_alias=org_alias)
        if not contexts:
            return self._insufficient_evidence(
                answer="No stored Deal Desk contexts are available yet to summarize quote-readiness blockers.",
                confidence=0.16,
                tools_used=["dealdesk.context.query"],
                follow_ups=[
                    "Why is opportunity 006XXXXXXXXXXXXXXX not quote-ready?",
                    "Show me recent deal changes.",
                ],
            )

        latest_contexts = self._latest_contexts_by_opportunity(contexts)
        blocked: list[tuple[DealContextV1, list[str], list[str]]] = []
        blocker_counts: dict[str, int] = {}
        for context in latest_contexts:
            ready, blockers, close_findings = self._context_readiness_snapshot(context)
            if ready:
                continue
            blocked.append((context, blockers, close_findings))
            for label in [*blockers, *close_findings]:
                blocker_counts[label] = blocker_counts.get(label, 0) + 1

        if not blocked:
            return RevOpsChatResponse(
                status="grounded",
                answer=f"All {len(latest_contexts)} stored Deal Desk context(s) currently look quote-ready.",
                confidence=0.79,
                sources=[self._context_source(context) for context in latest_contexts[:3]],
                tools_used=["dealdesk.context.query"],
                budget_used=RevOpsBudgetUsed(),
                follow_up_suggestions=[
                    "Show me recent deal changes.",
                    "Ask about a specific opportunity if you need blocker detail.",
                ],
            )

        blocked.sort(key=lambda item: _sort_timestamp(item[0].updated_at or item[0].created_at), reverse=True)
        blocker_summary = ", ".join(
            f"{label} ({count})"
            for label, count in sorted(blocker_counts.items(), key=lambda item: (-item[1], item[0]))[:3]
        )
        example_summary = "; ".join(
            f"`{context.opportunity_id}` in `{context.lifecycle_state}`"
            for context, _, _ in blocked[:3]
        )
        answer = (
            f"I found {len(blocked)} of {len(latest_contexts)} stored deal(s) that are not quote-ready. "
            f"Top blockers: {blocker_summary or 'local Deal Desk blockers are present but not labeled'}. "
            f"Recent examples: {example_summary}."
        )
        return RevOpsChatResponse(
            status="grounded",
            answer=answer,
            confidence=0.82,
            sources=[self._context_source(context) for context, _, _ in blocked[:3]],
            tools_used=["dealdesk.context.query"],
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "Why is opportunity 006XXXXXXXXXXXXXXX not quote-ready?",
                "Show me recent deal changes.",
            ],
        )

    def _answer_recent_changes(
        self,
        *,
        opportunity_id: str | None,
        org_alias: str | None,
    ) -> RevOpsChatResponse:
        if not opportunity_id:
            return self._insufficient_evidence(
                answer="Include a specific opportunity ID to inspect receipts and pattern events for recent change history.",
                confidence=0.12,
                follow_ups=[
                    "What changed on deal 006XXXXXXXXXXXXXXX recently?",
                    "Why is opportunity 006XXXXXXXXXXXXXXX not quote-ready?",
                ],
            )

        context = self._load_context_for_opportunity(opportunity_id, org_alias=org_alias)
        receipts = self._load_related_receipts(
            opportunity_id=opportunity_id,
            receipt_ids=context.receipt_ids if context is not None else [],
            org_alias=context.org_alias if context is not None else org_alias,
        )
        events = self._load_related_pattern_events(
            opportunity_id=opportunity_id,
            crm_org_id=context.crm_org_id if context is not None else None,
            org_alias=context.org_alias if context is not None else org_alias,
        )

        if not receipts and not events:
            sources = [self._context_source(context)] if context is not None else []
            tools_used = ["dealdesk.context.query"] if context is not None else []
            return self._insufficient_evidence(
                answer=(
                    f"I found no recent receipts or pattern update events for opportunity `{opportunity_id}`. "
                    "There is not enough grounded change history in local platform evidence yet."
                ),
                confidence=0.22 if context is not None else 0.14,
                sources=sources,
                tools_used=tools_used,
                follow_ups=[
                    "Open Activity to inspect nearby runs manually.",
                    "Ask why this opportunity is not quote-ready if Deal Desk context exists.",
                ],
            )

        latest_event = events[0] if events else None
        latest_receipt = receipts[0] if receipts else None
        answer_parts = [
            f"Recent grounded evidence for opportunity `{opportunity_id}` includes {len(events)} pattern event(s) and {len(receipts)} related receipt(s)."
        ]
        if latest_event is not None:
            answer_parts.append(
                f"Latest pattern event was `{latest_event.source_type}`{_at_time_suffix(latest_event.observed_at)}."
            )
        if latest_receipt is not None:
            latest_outcome = str(latest_receipt.get("outcome") or "unknown").strip() or "unknown"
            answer_parts.append(
                f"Latest execution receipt was `{latest_receipt.get('run_id') or 'unknown'}` with outcome `{latest_outcome}`{_at_time_suffix(_captured_at(latest_receipt))}."
            )
        answer = " ".join(answer_parts)

        sources = []
        if context is not None:
            sources.append(self._context_source(context))
        sources.extend(self._pattern_event_source(event) for event in events[:3])
        sources.extend(self._receipt_source(payload) for payload in receipts[:3])
        tools_used = ["receipts.query"]
        if context is not None:
            tools_used.append("dealdesk.context.query")
        confidence = 0.84 if receipts and events else 0.72
        return RevOpsChatResponse(
            status="grounded",
            answer=answer,
            confidence=confidence,
            sources=sources,
            tools_used=_unique_strings(tools_used),
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "Ask why this opportunity is not quote-ready to connect the change history to current blockers.",
                "Open Activity for the full receipt payloads and decision paths.",
            ],
        )

    def _answer_recent_changes_overview(self, *, org_alias: str | None) -> RevOpsChatResponse:
        receipts = self._load_all_opportunity_receipts(org_alias=org_alias)
        events = self._load_all_pattern_events(org_alias=org_alias)
        if not receipts and not events:
            return self._insufficient_evidence(
                answer="There is no stored receipt or pattern-event history yet to summarize recent deal changes.",
                confidence=0.16,
                tools_used=["receipts.query"],
                follow_ups=[
                    "What changed on deal 006XXXXXXXXXXXXXXX recently?",
                    "Which opportunities are not quote-ready right now?",
                ],
            )

        per_opportunity: dict[str, dict[str, Any]] = {}
        for event in events:
            opportunity_id = str(event.opportunity_id or "").strip()
            if not opportunity_id:
                continue
            bucket = per_opportunity.setdefault(
                opportunity_id,
                {"event_count": 0, "receipt_count": 0, "latest_ts": 0.0},
            )
            bucket["event_count"] += 1
            bucket["latest_ts"] = max(bucket["latest_ts"], _sort_timestamp(event.observed_at))
        for payload in receipts:
            opportunity_ids = _receipt_opportunity_targets(payload)
            if not opportunity_ids:
                continue
            captured = _sort_timestamp(_captured_at(payload))
            for opportunity_id in opportunity_ids:
                bucket = per_opportunity.setdefault(
                    opportunity_id,
                    {"event_count": 0, "receipt_count": 0, "latest_ts": 0.0},
                )
                bucket["receipt_count"] += 1
                bucket["latest_ts"] = max(bucket["latest_ts"], captured)

        ranked = sorted(
            per_opportunity.items(),
            key=lambda item: (item[1]["latest_ts"], item[1]["event_count"] + item[1]["receipt_count"]),
            reverse=True,
        )
        examples = "; ".join(
            f"`{opportunity_id}` ({stats['event_count']} event(s), {stats['receipt_count']} receipt(s))"
            for opportunity_id, stats in ranked[:3]
        )
        answer = (
            f"Recent grounded change history covers {len(ranked)} deal(s) with {len(events)} pattern event(s) and "
            f"{len(receipts)} receipt(s). Most active recent examples: {examples or 'no identifiable opportunity targets'}."
        )

        sources = [
            *[self._pattern_event_source(event) for event in events[:2]],
            *[self._receipt_source(payload) for payload in receipts[:2]],
        ]
        tools_used = ["receipts.query"]
        if org_alias:
            tools_used.append("dealdesk.context.query")
        return RevOpsChatResponse(
            status="grounded",
            answer=answer,
            confidence=0.79 if receipts and events else 0.7,
            sources=sources,
            tools_used=_unique_strings(tools_used),
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "What changed on deal 006XXXXXXXXXXXXXXX recently?",
                "Which opportunities are not quote-ready right now?",
            ],
        )

    def _answer_scout_summary(
        self,
        *,
        question: str,
        org_alias: str | None,
        window_label: str | None = None,
    ) -> RevOpsChatResponse:
        resolved_org_alias, scope_error, strict_scope = self._resolve_summary_org_scope(org_alias)
        if scope_error:
            return self._insufficient_evidence(
                answer=scope_error,
                confidence=0.14,
                tools_used=["dealdesk.context.query"],
                follow_ups=[
                    "Which scouts found issues today for org `prod`?",
                    "What did auto-pause do this week for org `prod`?",
                ],
            )

        window = self._resolve_summary_window(question, fallback_label=window_label)
        scans = sorted(self._load_scan_results(), key=_scan_sort_key, reverse=True)
        scoped_scans = [
            scan
            for scan in scans
            if self._scan_matches_org_alias(scan, org_alias=resolved_org_alias, strict=strict_scope)
        ]
        window_scans = [
            scan
            for scan in scoped_scans
            if (captured_at := _parse_iso(_captured_at(scan))) is not None and _summary_window_contains(window, captured_at)
        ]
        scope_suffix = f" for org `{resolved_org_alias}`" if resolved_org_alias else ""

        if not window_scans:
            sources = (
                [self._scout_run_summary_source(scoped_scans[0], window_label=window.label)]
                if scoped_scans
                else []
            )
            latest_suffix = ""
            if scoped_scans:
                latest_suffix = (
                    f" Latest stored scout run was `{scoped_scans[0].get('run_id') or 'unknown'}`"
                    f"{_at_time_suffix(_captured_at(scoped_scans[0]))}."
                )
            return self._insufficient_evidence(
                answer=(
                    f"I found no stored scout scan results in {window.label}{scope_suffix}.{latest_suffix}"
                ),
                confidence=0.2 if not scoped_scans else 0.44,
                sources=sources,
                tools_used=["scout.findings.query"],
                follow_ups=[
                    "Which scouts found issues this week?",
                    "Show me recent DNC violations.",
                ],
            )

        buckets = self._aggregate_scout_summary(window_scans)
        if not buckets:
            return RevOpsChatResponse(
                status="grounded",
                answer=(
                    f"No stored scout runs in {window.label}{scope_suffix} reported active findings. "
                    f"I checked {len(window_scans)} run(s)."
                ),
                confidence=0.84,
                sources=[
                    self._scout_run_summary_source(scan, window_label=window.label)
                    for scan in window_scans[:3]
                ],
                tools_used=["scout.findings.query"],
                budget_used=RevOpsBudgetUsed(),
                follow_up_suggestions=[
                    "Show me recent DNC violations.",
                    "Which renewals look stalled with zero activity?",
                ],
            )

        ranked = sorted(
            buckets.values(),
            key=lambda item: (item["latest_ts"], item["total_findings"], item["run_count"]),
            reverse=True,
        )
        total_findings = sum(int(bucket["total_findings"]) for bucket in ranked)
        examples = "; ".join(
            (
                f"`{bucket['scout_name']}`: {int(bucket['total_findings'])} finding(s) across "
                f"{int(bucket['run_count'])} run(s), highest severity `{bucket['highest_severity']}`, "
                f"latest run `{bucket['latest_run_id']}`{_at_time_suffix(bucket['latest_captured_at'])}"
            )
            for bucket in ranked[:3]
        )
        return RevOpsChatResponse(
            status="grounded",
            answer=(
                f"In {window.label}{scope_suffix}, {len(ranked)} scout(s) reported {total_findings} finding(s) "
                f"across {len(window_scans)} stored run(s). {examples}."
            ),
            confidence=0.88,
            sources=[
                self._scout_summary_source(bucket, window_label=window.label)
                for bucket in ranked[:3]
            ],
            tools_used=["scout.findings.query"],
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "Show me recent DNC violations.",
                "What did auto-pause do this week?",
            ],
        )

    def _answer_module_summary(
        self,
        *,
        module_id: str,
        question: str,
        org_alias: str | None,
        window_label: str | None = None,
    ) -> RevOpsChatResponse:
        resolved_org_alias, scope_error, strict_scope = self._resolve_summary_org_scope(org_alias)
        if scope_error:
            return self._insufficient_evidence(
                answer=scope_error,
                confidence=0.14,
                tools_used=["dealdesk.context.query"],
                follow_ups=[
                    "What did auto-pause do this week for org `prod`?",
                    "Which scouts found issues today for org `prod`?",
                ],
            )

        receipts = self._load_module_receipts(
            module_id=module_id,
            org_alias=resolved_org_alias,
            strict_scope=strict_scope,
        )
        display_name = _module_summary_display_name(module_id)
        scope_suffix = f" for org `{resolved_org_alias}`" if resolved_org_alias else ""
        if not receipts:
            return self._insufficient_evidence(
                answer=(
                    f"I found no stored `{module_id}` receipt history to summarize for {display_name}{scope_suffix}."
                ),
                confidence=0.18,
                tools_used=["receipts.query"],
                follow_ups=[
                    "Which scouts found issues today?",
                    "Show me recent DNC violations.",
                ],
            )

        window = self._resolve_summary_window(question, fallback_label=window_label)
        window_receipts = [
            payload
            for payload in receipts
            if (captured_at := _parse_iso(_captured_at(payload))) is not None and _summary_window_contains(window, captured_at)
        ]
        if not window_receipts:
            latest = receipts[0]
            latest_source = self._module_summary_source(
                module_id=module_id,
                receipts=[latest],
                window_label=window.label,
            )
            latest_outcome = str(latest.get("outcome") or "unknown").strip().lower() or "unknown"
            return RevOpsChatResponse(
                status="grounded",
                answer=(
                    f"No stored {display_name} receipts fall inside {window.label}{scope_suffix}. "
                    f"Latest available run was `{latest.get('run_id') or 'unknown'}` with outcome `{latest_outcome}`"
                    f"{_at_time_suffix(_captured_at(latest))}."
                ),
                confidence=0.82,
                sources=[latest_source],
                tools_used=["receipts.query"],
                budget_used=RevOpsBudgetUsed(),
                follow_up_suggestions=[
                    "Which scouts found issues today?",
                    "Show me recent DNC violations.",
                ],
            )

        outcome_counts: dict[str, int] = {}
        total_changes = 0
        for payload in window_receipts:
            outcome = str(payload.get("outcome") or "unknown").strip().lower() or "unknown"
            outcome_counts[outcome] = outcome_counts.get(outcome, 0) + 1
            total_changes += _receipt_change_count(payload)
        outcome_summary = _format_summary_counts(outcome_counts)
        latest = window_receipts[0]
        return RevOpsChatResponse(
            status="grounded",
            answer=(
                f"In {window.label}{scope_suffix}, stored {display_name} history shows {len(window_receipts)} "
                f"receipt(s): {outcome_summary}. Those receipts recorded {total_changes} change record(s). "
                f"Latest run was `{latest.get('run_id') or 'unknown'}`{_at_time_suffix(_captured_at(latest))}."
            ),
            confidence=0.87,
            sources=[
                self._module_summary_source(
                    module_id=module_id,
                    receipts=window_receipts,
                    window_label=window.label,
                ),
                *[self._receipt_source(payload) for payload in window_receipts[1:3]],
            ],
            tools_used=["receipts.query"],
            budget_used=RevOpsBudgetUsed(),
            follow_up_suggestions=[
                "Which scouts found issues today?",
                "Show me recent DNC violations.",
            ],
        )

    def _classify_question(self, message: str) -> _QuestionIntent:
        lowered = _normalize_message_text(message)
        opportunity_id = self._extract_opportunity_id(message)
        explicit_window_label = _detect_summary_window_label(lowered)
        candidates: list[_QuestionIntent] = []
        reset_signals, reset_score = _weighted_phrase_matches(lowered, _RESET_CONVERSATION_PHRASES)
        if reset_signals:
            candidates.append(
                _QuestionIntent(
                    intent="reset_conversation",
                    opportunity_id=opportunity_id,
                    matched_signals=tuple(reset_signals),
                    score=reset_score,
                )
            )
        evidence_signals, evidence_score = _weighted_phrase_matches(lowered, _SCOUT_EVIDENCE_PHRASES)
        approval_signals, approval_score = _weighted_phrase_matches(lowered, _APPROVAL_REQUEST_PHRASES)
        if approval_signals:
            candidates.append(
                _QuestionIntent(
                    intent="approve_proposal",
                    opportunity_id=opportunity_id,
                    matched_signals=tuple(approval_signals),
                    score=approval_score,
                )
            )

        scout_summary_signals, scout_summary_score = _weighted_phrase_matches(lowered, _SCOUT_SUMMARY_PHRASES)
        if scout_summary_signals and (
            _contains_phrase(lowered, _SUMMARY_WINDOW_FOLLOW_UP_PHRASES)
            or "scouts" in lowered
            or "summary" in lowered
        ):
            candidates.append(
                _QuestionIntent(
                    intent="scout_summary",
                    opportunity_id=opportunity_id,
                    summary_window_label=explicit_window_label,
                    matched_signals=tuple(scout_summary_signals),
                    score=scout_summary_score + 0.18,
                )
            )

        module_summary_request_signals, module_summary_request_score = _weighted_phrase_matches(
            lowered,
            _MODULE_SUMMARY_REQUEST_PHRASES,
        )
        for module_id, phrase_weights in _MODULE_SUMMARY_TARGET_PHRASES.items():
            module_summary_signals, module_summary_score = _weighted_phrase_matches(lowered, phrase_weights)
            if not module_summary_signals:
                continue
            if not module_summary_request_signals and not _contains_phrase(lowered, _SUMMARY_WINDOW_FOLLOW_UP_PHRASES):
                continue
            candidates.append(
                _QuestionIntent(
                    intent="module_summary",
                    opportunity_id=opportunity_id,
                    module_id=module_id,
                    summary_window_label=explicit_window_label,
                    matched_signals=tuple([*module_summary_request_signals, *module_summary_signals]),
                    score=module_summary_request_score + module_summary_score + 0.22,
                )
            )

        action_signals, action_score = _weighted_phrase_matches(lowered, _PAUSE_SEQUENCE_ACTION_PHRASES)
        if action_signals:
            action_key = resolve_deterministic_action_key(action_type="pause_sequence", scout_name="do_not_contact")
            candidates.append(
                _QuestionIntent(
                    intent="suggest_action",
                    opportunity_id=opportunity_id,
                    action_key=action_key,
                    matched_signals=tuple(action_signals),
                    score=action_score,
                )
            )

        follow_up_action_signals, follow_up_action_score = _weighted_phrase_matches(lowered, _FOLLOW_UP_TASK_ACTION_PHRASES)
        if follow_up_action_signals:
            action_key = resolve_deterministic_action_key(
                action_type="followup_enforcement",
                scout_name="meeting_followup_breach",
            )
            candidates.append(
                _QuestionIntent(
                    intent="suggest_action",
                    opportunity_id=opportunity_id,
                    action_key=action_key,
                    matched_signals=tuple(follow_up_action_signals),
                    score=follow_up_action_score,
                )
            )

        route_action_signals, route_action_score = _weighted_phrase_matches(lowered, _ROUTE_EXCEPTION_ACTION_PHRASES)
        if route_action_signals:
            action_key = resolve_deterministic_action_key(
                action_type="route_exception",
                scout_name="customer_open_ticket_guardrail",
            )
            candidates.append(
                _QuestionIntent(
                    intent="suggest_action",
                    opportunity_id=opportunity_id,
                    action_key=action_key,
                    matched_signals=tuple(route_action_signals),
                    score=route_action_score,
                )
            )

        quote_signals, quote_score = _weighted_phrase_matches(lowered, _QUOTE_READINESS_PHRASES)
        if "readiness" in lowered and "quote" in lowered and "readiness" not in quote_signals:
            quote_signals = [*quote_signals, "readiness"]
            quote_score += 0.84
        if quote_signals:
            candidates.append(
                _QuestionIntent(
                    intent=(
                        "quote_readiness"
                        if opportunity_id or _contains_phrase(lowered, _SINGULAR_DEAL_REFERENCES)
                        else "quote_readiness_overview"
                    ),
                    opportunity_id=opportunity_id,
                    matched_signals=tuple(quote_signals),
                    score=quote_score + (0.25 if opportunity_id else 0.0),
                )
            )

        change_signals, change_score = _weighted_phrase_matches(lowered, _RECENT_CHANGE_PHRASES)
        if change_signals:
            candidates.append(
                _QuestionIntent(
                    intent=(
                        "recent_changes"
                        if opportunity_id or _contains_phrase(lowered, _SINGULAR_DEAL_REFERENCES)
                        else "recent_changes_overview"
                    ),
                    opportunity_id=opportunity_id,
                    matched_signals=tuple(change_signals),
                    score=change_score + (0.25 if opportunity_id else 0.0),
                )
            )

        dnc_signals, dnc_score = _weighted_phrase_matches(lowered, _DNC_PHRASES)
        if dnc_signals:
            candidates.append(
                _QuestionIntent(
                    intent="scout_evidence" if evidence_signals else "scout",
                    scout_name="do_not_contact",
                    opportunity_id=opportunity_id,
                    matched_signals=tuple([*dnc_signals, *evidence_signals]),
                    score=dnc_score + (evidence_score if evidence_signals else 0.0),
                )
            )

        renewal_signals, renewal_score = _weighted_phrase_matches(lowered, _RENEWAL_PHRASES)
        inactivity_signals, inactivity_score = _weighted_phrase_matches(lowered, _NO_ACTIVITY_PHRASES)
        if renewal_signals and inactivity_signals:
            candidates.append(
                _QuestionIntent(
                    intent="scout_evidence" if evidence_signals else "scout",
                    scout_name="renewals_due_no_activity",
                    opportunity_id=opportunity_id,
                    matched_signals=tuple([*renewal_signals, *inactivity_signals, *evidence_signals]),
                    score=renewal_score + inactivity_score + (evidence_score if evidence_signals else 0.0) + 0.18,
                )
            )

        follow_up_breach_signals, follow_up_breach_score = _weighted_phrase_matches(lowered, _FOLLOW_UP_BREACH_PHRASES)
        if follow_up_breach_signals:
            candidates.append(
                _QuestionIntent(
                    intent="scout_evidence" if evidence_signals else "scout",
                    scout_name="meeting_followup_breach",
                    opportunity_id=opportunity_id,
                    matched_signals=tuple([*follow_up_breach_signals, *evidence_signals]),
                    score=follow_up_breach_score + (evidence_score if evidence_signals else 0.0) + 0.12,
                )
            )

        field_drift_signals, field_drift_score = _weighted_phrase_matches(lowered, _FIELD_DRIFT_PHRASES)
        if field_drift_signals:
            candidates.append(
                _QuestionIntent(
                    intent="scout_evidence" if evidence_signals else "scout",
                    scout_name="cross_system_field_drift",
                    opportunity_id=opportunity_id,
                    matched_signals=tuple([*field_drift_signals, *evidence_signals]),
                    score=field_drift_score + (evidence_score if evidence_signals else 0.0) + 0.12,
                )
            )

        customer_guardrail_signals, customer_guardrail_score = _weighted_phrase_matches(lowered, _CUSTOMER_GUARDRAIL_PHRASES)
        if customer_guardrail_signals:
            candidates.append(
                _QuestionIntent(
                    intent="scout_evidence" if evidence_signals else "scout",
                    scout_name="customer_open_ticket_guardrail",
                    opportunity_id=opportunity_id,
                    matched_signals=tuple([*customer_guardrail_signals, *evidence_signals]),
                    score=customer_guardrail_score + (evidence_score if evidence_signals else 0.0) + 0.12,
                )
            )

        if not candidates:
            return _QuestionIntent(intent="unknown", opportunity_id=opportunity_id)
        return max(
            candidates,
            key=lambda candidate: (
                candidate.score,
                _INTENT_PRIORITY.get(candidate.intent, 0),
                len(candidate.matched_signals),
            ),
        )

    def _hydrate_intent_from_conversation(
        self,
        parsed: _QuestionIntent,
        *,
        question: str,
        conversation: _ConversationScope,
    ) -> _QuestionIntent:
        last_opportunity_id = str(conversation.last_opportunity_id or "").strip()
        last_scout_name = str(conversation.last_scout_name or "").strip()
        last_module_id = str(conversation.last_module_id or "").strip()
        last_summary_intent = str(conversation.last_summary_intent or "").strip()
        last_summary_window = str(conversation.last_summary_window or "").strip()
        if parsed.opportunity_id or (
            not last_opportunity_id and not last_scout_name and not last_module_id and not last_summary_intent
        ):
            return parsed
        lowered = _normalize_message_text(question)
        explicit_window_label = _detect_summary_window_label(lowered)

        if parsed.intent == "module_summary" and not parsed.module_id and last_module_id:
            return _QuestionIntent(
                intent="module_summary",
                module_id=last_module_id,
                summary_window_label=parsed.summary_window_label or explicit_window_label or last_summary_window or None,
                matched_signals=parsed.matched_signals,
                score=parsed.score + 0.14,
            )

        if parsed.intent == "unknown":
            summary_follow_up = self._summary_intent_from_conversation(
                lowered=lowered,
                conversation=conversation,
                explicit_window_label=explicit_window_label,
            )
            if summary_follow_up is not None:
                return summary_follow_up

        if _looks_like_overview_request(lowered):
            return parsed
        if parsed.intent == "quote_readiness_overview":
            return _QuestionIntent(
                intent="quote_readiness",
                opportunity_id=last_opportunity_id,
                matched_signals=parsed.matched_signals,
                score=parsed.score + 0.18,
            )
        if parsed.intent == "recent_changes_overview":
            return _QuestionIntent(
                intent="recent_changes",
                opportunity_id=last_opportunity_id,
                matched_signals=parsed.matched_signals,
                score=parsed.score + 0.18,
            )
        if parsed.intent != "unknown":
            return parsed
        if last_scout_name and any(hint in lowered for hint in _SCOUT_EVIDENCE_FOLLOW_UP_HINTS):
            return _QuestionIntent(
                intent="scout_evidence",
                scout_name=last_scout_name,
                matched_signals=("conversation_follow_up",),
                score=0.78,
            )
        if any(hint in lowered for hint in _RECENT_CHANGE_FOLLOW_UP_HINTS):
            return _QuestionIntent(
                intent="recent_changes",
                opportunity_id=last_opportunity_id,
                matched_signals=("conversation_follow_up",),
                score=0.76,
            )
        if any(hint in lowered for hint in _QUOTE_READINESS_FOLLOW_UP_HINTS) or lowered in _SHORT_QUOTE_READINESS_FOLLOW_UPS:
            return _QuestionIntent(
                intent="quote_readiness",
                opportunity_id=last_opportunity_id,
                matched_signals=("conversation_follow_up",),
                score=0.74,
            )
        return parsed

    def _summary_intent_from_conversation(
        self,
        *,
        lowered: str,
        conversation: _ConversationScope,
        explicit_window_label: str | None,
    ) -> _QuestionIntent | None:
        last_summary_intent = str(conversation.last_summary_intent or "").strip()
        if last_summary_intent not in {"scout_summary", "module_summary"}:
            return None
        if not (explicit_window_label or any(hint in lowered for hint in _SUMMARY_FOLLOW_UP_HINTS)):
            return None
        last_module_id = str(conversation.last_module_id or "").strip() or None
        fallback_window = explicit_window_label or str(conversation.last_summary_window or "").strip() or None
        if last_summary_intent == "module_summary":
            if not last_module_id:
                return None
            return _QuestionIntent(
                intent="module_summary",
                module_id=last_module_id,
                summary_window_label=fallback_window,
                matched_signals=("conversation_summary_follow_up",),
                score=0.82,
            )
        return _QuestionIntent(
            intent="scout_summary",
            summary_window_label=fallback_window,
            matched_signals=("conversation_summary_follow_up",),
            score=0.8,
        )

    def _extract_opportunity_id(self, message: str) -> str | None:
        match = _SALESFORCE_OPPORTUNITY_ID_RE.search(message)
        if match:
            return match.group(0)
        for token_match in _OPPORTUNITY_TOKEN_RE.finditer(message):
            candidate = str(token_match.group(1) or "").strip().rstrip("?.!,")
            if candidate.lower() in _STOP_ENTITY_TOKENS:
                continue
            if not _looks_like_specific_entity_token(candidate):
                continue
            return candidate
        return None

    def _latest_scan_finding_for_scout(self, scout_name: str) -> tuple[dict[str, Any], dict[str, Any], int] | None:
        scout_key = str(scout_name or "").strip()
        if not scout_key:
            return None
        scans = sorted(self._load_scan_results(), key=_scan_sort_key, reverse=True)
        for scan in scans:
            findings = scan.get("findings")
            if not isinstance(findings, list):
                continue
            for finding_index, finding in enumerate(findings):
                if not isinstance(finding, dict):
                    continue
                if str(finding.get("scout_name") or "").strip() == scout_key:
                    return scan, finding, finding_index
        return None

    def _load_scan_results(self) -> list[dict[str, Any]]:
        receipts_dir = self.state_dir / "receipts"
        if not receipts_dir.exists():
            return []
        scans: list[dict[str, Any]] = []
        for run_dir in receipts_dir.iterdir():
            if not run_dir.is_dir():
                continue
            payload = _load_json_dict(run_dir / "scan.json")
            if payload is None:
                continue
            payload.setdefault("run_id", run_dir.name)
            scans.append(payload)
        return scans

    def _load_scan_payload(self, *, run_id: str) -> dict[str, Any] | None:
        cleaned = str(run_id or "").strip()
        if not cleaned:
            return None
        payload = _load_json_dict(self.state_dir / "receipts" / cleaned / "scan.json")
        if payload is None:
            return None
        payload.setdefault("run_id", cleaned)
        return payload

    def _load_grounded_action_context(
        self,
        *,
        conversation: _ConversationScope,
        org_alias: str | None,
        action_key: str,
    ) -> tuple[dict[str, Any], dict[str, Any], int, list[dict[str, Any]]] | None:
        grounded_scout_name = str(conversation.last_grounded_scout_name or "").strip()
        supported_scouts = _GROUND_CHAT_ACTION_SCOUTS.get(action_key) or set()
        if grounded_scout_name not in supported_scouts:
            return None
        run_id = str(conversation.last_grounded_run_id or "").strip()
        finding_index = conversation.last_grounded_finding_index
        if not run_id or finding_index is None:
            return None
        scan_payload = self._load_scan_payload(run_id=run_id)
        if scan_payload is None:
            return None
        scan_org_alias = str(scan_payload.get("org_alias") or "").strip() or None
        if org_alias and scan_org_alias != org_alias:
            return None
        findings = scan_payload.get("findings") if isinstance(scan_payload.get("findings"), list) else []
        if finding_index < 0 or finding_index >= len(findings):
            return None
        finding = findings[finding_index]
        if not isinstance(finding, dict):
            return None
        if str(finding.get("scout_name") or "").strip() != grounded_scout_name:
            return None
        evidence_rows = finding.get("evidence") if isinstance(finding.get("evidence"), list) else []
        scoped_rows = [
            row
            for row in evidence_rows
            if isinstance(row, dict) and _row_record_id(row)
        ]
        if not scoped_rows:
            return None
        if action_key != "sequence_pause" and not self._matching_recommended_actions(
            finding=finding,
            action_key=action_key,
            scout_name=grounded_scout_name,
        ):
            return None
        return scan_payload, finding, finding_index, scoped_rows

    def _load_context_for_opportunity(
        self,
        opportunity_id: str,
        *,
        org_alias: str | None,
    ) -> DealContextV1 | None:
        if org_alias:
            try:
                direct = self.store.load_context(opportunity_id=opportunity_id, org_alias=org_alias)
            except Exception:
                direct = None
            if direct is not None:
                return direct

        root = self.state_dir / "dealdesk" / "contexts"
        if not root.exists():
            return None
        matches: list[DealContextV1] = []
        for path in root.glob("*/*.json"):
            payload = _load_json_dict(path)
            if payload is None:
                continue
            if str(payload.get("opportunity_id") or "").strip() != opportunity_id:
                continue
            if org_alias and str(payload.get("org_alias") or "").strip() != org_alias:
                continue
            try:
                matches.append(DealContextV1.from_dict(payload))
            except Exception:
                continue
        if not matches:
            return None
        matches.sort(key=lambda item: _sort_timestamp(item.updated_at or item.created_at), reverse=True)
        return matches[0]

    def _load_all_contexts(self, *, org_alias: str | None) -> list[DealContextV1]:
        root = self.state_dir / "dealdesk" / "contexts"
        if not root.exists():
            return []
        contexts: list[DealContextV1] = []
        for path in root.glob("*/*.json"):
            payload = _load_json_dict(path)
            if payload is None:
                continue
            if org_alias and str(payload.get("org_alias") or "").strip() != org_alias:
                continue
            try:
                contexts.append(DealContextV1.from_dict(payload))
            except Exception:
                continue
        contexts.sort(key=lambda item: _sort_timestamp(item.updated_at or item.created_at), reverse=True)
        return contexts

    def _latest_contexts_by_opportunity(self, contexts: list[DealContextV1]) -> list[DealContextV1]:
        latest: dict[str, DealContextV1] = {}
        for context in contexts:
            opportunity_id = str(context.opportunity_id or "").strip()
            if not opportunity_id or opportunity_id in latest:
                continue
            latest[opportunity_id] = context
        return list(latest.values())

    def _org_scoped_opportunity_ids(self, *, org_alias: str) -> set[str]:
        scoped_contexts = self._load_all_contexts(org_alias=org_alias)
        return {
            str(context.opportunity_id or "").strip()
            for context in scoped_contexts
            if str(context.opportunity_id or "").strip()
        }

    def _load_related_pattern_events(
        self,
        *,
        opportunity_id: str,
        crm_org_id: str | None,
        org_alias: str | None,
    ) -> list[PatternUpdateEventV1]:
        events: list[PatternUpdateEventV1] = []
        scoped_opportunity_ids: set[str] | None = None
        if org_alias and not crm_org_id:
            scoped_opportunity_ids = self._org_scoped_opportunity_ids(org_alias=org_alias)
            if not scoped_opportunity_ids or opportunity_id not in scoped_opportunity_ids:
                return []

        if crm_org_id:
            try:
                scoped = self.store.list_pattern_update_events(crm_org_id=crm_org_id, org_alias=None)
            except Exception:
                scoped = []
            events.extend(event for event in scoped if str(event.opportunity_id or "").strip() == opportunity_id)
        else:
            root = self.state_dir / "dealdesk" / "events"
            if root.exists():
                for path in root.glob("*/*.json"):
                    payload = _load_json_dict(path)
                    if payload is None:
                        continue
                    if str(payload.get("opportunity_id") or "").strip() != opportunity_id:
                        continue
                    if scoped_opportunity_ids is not None and opportunity_id not in scoped_opportunity_ids:
                        continue
                    try:
                        events.append(PatternUpdateEventV1.from_dict(payload))
                    except Exception:
                        continue
        events.sort(key=lambda item: _sort_timestamp(item.observed_at), reverse=True)
        return events

    def _load_related_receipts(
        self,
        *,
        opportunity_id: str,
        receipt_ids: list[str],
        org_alias: str | None,
    ) -> list[dict[str, Any]]:
        wanted_ids = {str(value).strip() for value in receipt_ids if str(value).strip()}
        scoped_opportunity_ids: set[str] | None = None
        if org_alias:
            scoped_opportunity_ids = self._org_scoped_opportunity_ids(org_alias=org_alias)
            if not scoped_opportunity_ids or opportunity_id not in scoped_opportunity_ids:
                return []
        receipts_dir = self.state_dir / "receipts"
        if not receipts_dir.exists():
            return []
        rows: list[dict[str, Any]] = []
        for run_dir in receipts_dir.iterdir():
            if not run_dir.is_dir():
                continue
            payload = _load_json_dict(run_dir / "receipt.json")
            if payload is None:
                continue
            run_id = str(payload.get("run_id") or run_dir.name).strip()
            targets = _receipt_opportunity_targets(payload)
            if scoped_opportunity_ids is not None and not any(target in scoped_opportunity_ids for target in targets):
                if run_id not in wanted_ids:
                    continue
            if run_id in wanted_ids or opportunity_id in targets:
                payload.setdefault("run_id", run_id)
                rows.append(payload)
        rows.sort(key=_receipt_sort_key, reverse=True)
        return rows

    def _load_all_opportunity_receipts(self, *, org_alias: str | None) -> list[dict[str, Any]]:
        receipts_dir = self.state_dir / "receipts"
        if not receipts_dir.exists():
            return []
        scoped_opportunity_ids: set[str] | None = None
        if org_alias:
            scoped_opportunity_ids = self._org_scoped_opportunity_ids(org_alias=org_alias)
            if not scoped_opportunity_ids:
                return []
        rows: list[dict[str, Any]] = []
        for run_dir in receipts_dir.iterdir():
            if not run_dir.is_dir():
                continue
            payload = _load_json_dict(run_dir / "receipt.json")
            if payload is None:
                continue
            targets = _receipt_opportunity_targets(payload)
            if not targets:
                continue
            if scoped_opportunity_ids is not None and not any(target in scoped_opportunity_ids for target in targets):
                continue
            payload.setdefault("run_id", run_dir.name)
            rows.append(payload)
        rows.sort(key=_receipt_sort_key, reverse=True)
        return rows

    def _load_all_pattern_events(self, *, org_alias: str | None) -> list[PatternUpdateEventV1]:
        root = self.state_dir / "dealdesk" / "events"
        if not root.exists():
            return []
        scoped_opportunity_ids: set[str] | None = None
        if org_alias:
            scoped_opportunity_ids = self._org_scoped_opportunity_ids(org_alias=org_alias)
            if not scoped_opportunity_ids:
                return []
        events: list[PatternUpdateEventV1] = []
        for path in root.glob("*/*.json"):
            payload = _load_json_dict(path)
            if payload is None:
                continue
            opportunity_id = str(payload.get("opportunity_id") or "").strip()
            if scoped_opportunity_ids is not None and opportunity_id not in scoped_opportunity_ids:
                continue
            try:
                events.append(PatternUpdateEventV1.from_dict(payload))
            except Exception:
                continue
        events.sort(key=lambda item: _sort_timestamp(item.observed_at), reverse=True)
        return events

    def _resolve_summary_org_scope(self, org_alias: str | None) -> tuple[str | None, str | None, bool]:
        cleaned = str(org_alias or "").strip() or None
        if cleaned:
            return cleaned, None, True
        known_aliases = self._known_summary_org_aliases()
        if len(known_aliases) > 1:
            return (
                None,
                "This summary needs an explicit `org_alias` because the stored evidence spans multiple orgs.",
                False,
            )
        if len(known_aliases) == 1:
            return known_aliases[0], None, False
        return None, None, False

    def _known_summary_org_aliases(self) -> list[str]:
        aliases: set[str] = set()
        for context in self._load_all_contexts(org_alias=None):
            alias = str(context.org_alias or "").strip()
            if alias:
                aliases.add(alias)
        for scan in self._load_scan_results():
            alias = str(scan.get("org_alias") or "").strip()
            if alias:
                aliases.add(alias)
        receipts_dir = self.state_dir / "receipts"
        if receipts_dir.exists():
            for run_dir in receipts_dir.iterdir():
                if not run_dir.is_dir():
                    continue
                alias = self._plan_org_alias(self._load_plan_payload(run_id=run_dir.name))
                if alias:
                    aliases.add(alias)
        return sorted(aliases)

    def _scan_matches_org_alias(
        self,
        scan_payload: dict[str, Any],
        *,
        org_alias: str | None,
        strict: bool,
    ) -> bool:
        if not org_alias:
            return True
        scan_org_alias = str(scan_payload.get("org_alias") or "").strip() or None
        if scan_org_alias:
            return scan_org_alias == org_alias
        return not strict

    def _load_plan_payload(self, *, run_id: str) -> dict[str, Any] | None:
        cleaned = str(run_id or "").strip()
        if not cleaned:
            return None
        payload = _load_json_dict(self.state_dir / "receipts" / cleaned / "plan.json")
        if payload is None:
            return None
        payload.setdefault("run_id", cleaned)
        return payload

    def _plan_org_alias(self, plan_payload: dict[str, Any] | None) -> str | None:
        if not isinstance(plan_payload, dict):
            return None
        direct = str(plan_payload.get("org_alias") or "").strip()
        if direct:
            return direct
        raw_steps = plan_payload.get("steps") if isinstance(plan_payload.get("steps"), list) else []
        aliases = {
            str(args.get("org_alias") or "").strip()
            for step in raw_steps
            if isinstance(step, dict)
            and isinstance((args := step.get("args")), dict)
            and str(args.get("org_alias") or "").strip()
        }
        if len(aliases) == 1:
            return next(iter(aliases))
        return None

    def _receipt_org_alias(self, payload: dict[str, Any]) -> str | None:
        direct = str(payload.get("org_alias") or "").strip()
        if direct:
            return direct
        run_id = str(payload.get("run_id") or "").strip()
        if not run_id:
            return None
        return self._plan_org_alias(self._load_plan_payload(run_id=run_id))

    def _receipt_matches_org_alias(
        self,
        payload: dict[str, Any],
        *,
        org_alias: str | None,
        strict: bool,
    ) -> bool:
        if not org_alias:
            return True
        receipt_org_alias = self._receipt_org_alias(payload)
        if receipt_org_alias:
            return receipt_org_alias == org_alias
        return not strict

    def _load_module_receipts(
        self,
        *,
        module_id: str,
        org_alias: str | None,
        strict_scope: bool,
    ) -> list[dict[str, Any]]:
        receipts_dir = self.state_dir / "receipts"
        if not receipts_dir.exists():
            return []
        rows: list[dict[str, Any]] = []
        for run_dir in receipts_dir.iterdir():
            if not run_dir.is_dir():
                continue
            payload = _load_json_dict(run_dir / "receipt.json")
            if payload is None:
                continue
            payload.setdefault("run_id", run_dir.name)
            if module_id not in _receipt_module_ids(payload):
                continue
            if not self._receipt_matches_org_alias(payload, org_alias=org_alias, strict=strict_scope):
                continue
            rows.append(payload)
        rows.sort(key=_receipt_sort_key, reverse=True)
        return rows

    def _resolve_summary_window(self, question: str, *, fallback_label: str | None = None) -> _SummaryWindow:
        lowered = _normalize_message_text(question)
        label = _detect_summary_window_label(lowered) or fallback_label or "the last 7 days"
        return _summary_window_from_label(label)

    def _aggregate_scout_summary(self, scans: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
        buckets: dict[str, dict[str, Any]] = {}
        for scan in scans:
            findings = scan.get("findings") if isinstance(scan.get("findings"), list) else []
            run_id = str(scan.get("run_id") or "").strip() or "scan"
            captured_at = _captured_at(scan)
            captured_ts = _sort_timestamp(captured_at)
            for finding_index, finding in enumerate(findings):
                if not isinstance(finding, dict):
                    continue
                scout_name = str(finding.get("scout_name") or "").strip()
                if not scout_name:
                    continue
                findings_count = _coerce_int(finding.get("findings_count"), 0)
                status = str(finding.get("status") or "").strip().lower() or "unknown"
                if findings_count <= 0 or status == "ok":
                    continue
                severity = str(finding.get("severity") or "").strip().lower() or "unknown"
                bucket = buckets.setdefault(
                    scout_name,
                    {
                        "scout_name": scout_name,
                        "total_findings": 0,
                        "run_ids": set(),
                        "run_count": 0,
                        "highest_severity": severity,
                        "highest_severity_rank": _severity_rank(severity),
                        "latest_ts": 0.0,
                        "latest_run_id": run_id,
                        "latest_captured_at": captured_at,
                        "latest_scan": scan,
                        "latest_finding": finding,
                        "latest_finding_index": finding_index,
                    },
                )
                bucket["total_findings"] += findings_count
                bucket["run_ids"].add(run_id)
                bucket["run_count"] = len(bucket["run_ids"])
                severity_rank = _severity_rank(severity)
                if severity_rank > int(bucket["highest_severity_rank"]):
                    bucket["highest_severity"] = severity
                    bucket["highest_severity_rank"] = severity_rank
                if captured_ts >= float(bucket["latest_ts"]):
                    bucket["latest_ts"] = captured_ts
                    bucket["latest_run_id"] = run_id
                    bucket["latest_captured_at"] = captured_at
                    bucket["latest_scan"] = scan
                    bucket["latest_finding"] = finding
                    bucket["latest_finding_index"] = finding_index
        return buckets

    def _scout_summary_source(
        self,
        bucket: dict[str, Any],
        *,
        window_label: str,
    ) -> RevOpsChatSource:
        latest_scan = bucket["latest_scan"]
        latest_finding = bucket["latest_finding"]
        latest_finding_index = int(bucket["latest_finding_index"])
        base = self._build_scout_sources(latest_scan, latest_finding, latest_finding_index)[0]
        metadata = {
            **base.metadata,
            "window_label": window_label,
            "aggregated_findings_count": int(bucket["total_findings"]),
            "aggregated_run_count": int(bucket["run_count"]),
            "aggregated_run_ids": sorted(str(value) for value in bucket["run_ids"]),
            "highest_severity": bucket["highest_severity"],
        }
        return base.model_copy(
            update={
                "title": f"{_humanize_token(bucket['scout_name'])} summary",
                "summary": (
                    f"{int(bucket['total_findings'])} finding(s) across {int(bucket['run_count'])} run(s) "
                    f"in {window_label}; highest severity `{bucket['highest_severity']}`."
                ),
                "metadata": metadata,
            }
        )

    def _scout_run_summary_source(
        self,
        scan_payload: dict[str, Any],
        *,
        window_label: str,
    ) -> RevOpsChatSource:
        run_id = str(scan_payload.get("run_id") or "unknown").strip() or "unknown"
        findings = scan_payload.get("findings") if isinstance(scan_payload.get("findings"), list) else []
        active_findings = 0
        for finding in findings:
            if not isinstance(finding, dict):
                continue
            findings_count = _coerce_int(finding.get("findings_count"), 0)
            status = str(finding.get("status") or "").strip().lower() or "unknown"
            if findings_count > 0 and status != "ok":
                active_findings += findings_count
        return RevOpsChatSource(
            source_id=run_id,
            source_type="scout_run",
            title=f"Scout run {run_id}",
            summary=(
                f"Stored scout run in {window_label} with {active_findings} active finding(s) "
                f"across {len(findings)} finding group(s)."
            ),
            captured_at=_captured_at(scan_payload),
            metadata={
                "run_id": run_id,
                "window_label": window_label,
                "finding_groups": len(findings),
                "active_findings": active_findings,
                "org_alias": scan_payload.get("org_alias"),
            },
        )

    def _module_summary_source(
        self,
        *,
        module_id: str,
        receipts: list[dict[str, Any]],
        window_label: str,
    ) -> RevOpsChatSource:
        latest = receipts[0]
        base = self._receipt_source(latest)
        outcome_counts: dict[str, int] = {}
        total_changes = 0
        for payload in receipts:
            outcome = str(payload.get("outcome") or "unknown").strip().lower() or "unknown"
            outcome_counts[outcome] = outcome_counts.get(outcome, 0) + 1
            total_changes += _receipt_change_count(payload)
        return base.model_copy(
            update={
                "title": f"{_module_summary_display_name(module_id)} receipt summary",
                "summary": (
                    f"{len(receipts)} receipt(s) in {window_label}; outcomes { _format_summary_counts(outcome_counts) }."
                ),
                "metadata": {
                    **base.metadata,
                    "module_id": module_id,
                    "window_label": window_label,
                    "receipt_count": len(receipts),
                    "outcome_counts": outcome_counts,
                    "total_change_records": total_changes,
                    "run_ids": [str(payload.get('run_id') or 'unknown') for payload in receipts[:6]],
                },
            }
        )

    def _lookup_opportunity_snapshot(
        self,
        *,
        opportunity_id: str,
        org_alias: str | None,
        budget: RevOpsBudgetUsed,
    ) -> tuple[RevOpsChatSource | None, dict[str, str] | None]:
        if budget.live_salesforce_queries >= budget.max_live_salesforce_queries:
            budget.budget_exceeded = True
            budget.notes.append("Live Salesforce query budget exhausted before fallback.")
            return None, None
        remaining_rows = budget.max_live_salesforce_rows - budget.live_salesforce_rows
        if remaining_rows <= 0:
            budget.budget_exceeded = True
            budget.notes.append("Live Salesforce row budget exhausted before fallback.")
            return None, None

        safe_id = opportunity_id.replace("\\", "\\\\").replace("'", "\\'")
        soql = (
            "SELECT Id, Name, StageName, Amount, CloseDate, LastModifiedDate, Owner.Name, AccountId "
            f"FROM Opportunity WHERE Id = '{safe_id}' LIMIT {min(1, remaining_rows)}"
        )
        start = time.perf_counter()
        try:
            rows = self._soql_runner(
                soql,
                org_alias=org_alias,
                state_dir=self.state_dir,
                timeout_seconds=max(0.25, budget.max_live_salesforce_time_ms / 1000.0),
            )
        except Exception as exc:
            budget.fallback_used = True
            budget.live_salesforce_queries += 1
            budget.notes.append(f"Salesforce fallback unavailable: {exc}")
            return None, None

        elapsed_ms = int((time.perf_counter() - start) * 1000)
        budget.fallback_used = True
        budget.live_salesforce_queries += 1
        budget.live_salesforce_time_ms += elapsed_ms
        if elapsed_ms > budget.max_live_salesforce_time_ms:
            budget.budget_exceeded = True
            budget.notes.append("Salesforce fallback exceeded the time budget.")
            return None, None

        trimmed = list(rows[:remaining_rows])
        budget.live_salesforce_rows += len(trimmed)
        if len(rows) > remaining_rows:
            budget.budget_exceeded = True
            budget.notes.append("Salesforce fallback was truncated to the remaining row budget.")

        if not trimmed:
            return None, None
        row = trimmed[0]
        source = RevOpsChatSource(
            source_id=f"live-soql:{opportunity_id}",
            source_type="salesforce_query",
            title="Live Salesforce opportunity lookup",
            summary="Single guarded Opportunity read used because local Deal Desk evidence was missing.",
            captured_at=datetime.now(timezone.utc).isoformat(),
            metadata={
                "object": "Opportunity",
                "query_budget": f"{budget.live_salesforce_queries}/{budget.max_live_salesforce_queries}",
                "row_budget": f"{budget.live_salesforce_rows}/{budget.max_live_salesforce_rows}",
                "stage_name": row.get("StageName"),
                "opportunity_name": row.get("Name"),
            },
        )
        return source, row

    def _context_source(self, context: DealContextV1) -> RevOpsChatSource:
        open_assumptions = [
            _humanize_token(item.key)
            for item in context.assumptions
            if item.status != "confirmed" or item.value in (None, "", [], {})
        ]
        return RevOpsChatSource(
            source_id=context.context_id,
            source_type="dealdesk_context",
            title=f"Deal context {context.context_id}",
            summary=(
                f"Lifecycle `{context.lifecycle_state}` with {len(open_assumptions)} open assumption(s) "
                f"and {len(context.receipt_ids)} related receipt(s)."
            ),
            captured_at=context.updated_at or context.created_at,
            metadata={
                "opportunity_id": context.opportunity_id,
                "lifecycle_state": context.lifecycle_state,
                "open_assumptions": open_assumptions[:4],
                "close_findings": [_humanize_token(value) for value in context.close_findings[:4]],
                "confidence": round(float(context.confidence_at_creation or context.inferred_motion_confidence or 0.0), 2),
            },
        )

    def _pattern_profile_source(self, profile: DealPatternProfileV1) -> RevOpsChatSource:
        return RevOpsChatSource(
            source_id=profile.profile_id,
            source_type="dealdesk_pattern_profile",
            title=f"Pattern profile {profile.profile_id}",
            summary=(
                f"Learning state `{profile.learning_state}` across {profile.source_record_count} historical deal(s) "
                f"and {profile.source_receipt_count} receipt-backed updates."
            ),
            captured_at=profile.updated_at or profile.generated_at,
            metadata={
                "learning_state": profile.learning_state,
                "source_record_count": profile.source_record_count,
                "source_receipt_count": profile.source_receipt_count,
                "profile_confidence": round(float(profile.profile_confidence), 2),
            },
        )

    def _environment_profile_source(self, profile: QuoteEnvironmentProfileV1) -> RevOpsChatSource:
        return RevOpsChatSource(
            source_id=profile.profile_id,
            source_type="dealdesk_environment_profile",
            title=f"Quote environment {profile.profile_id}",
            summary=(
                f"Detected engine `{profile.detected_engine}` with {len(profile.field_mappings)} field mapping candidate(s)."
            ),
            captured_at=profile.updated_at or profile.generated_at,
            metadata={
                "detected_engine": profile.detected_engine,
                "engine_confidence": round(float(profile.engine_confidence), 2),
                "quote_objects": profile.quote_objects[:4],
                "unsupported_reasons": profile.unsupported_reasons[:3],
            },
        )

    def _pattern_event_source(self, event: PatternUpdateEventV1) -> RevOpsChatSource:
        return RevOpsChatSource(
            source_id=event.event_id,
            source_type="pattern_update_event",
            title=f"Pattern event {event.event_id}",
            summary=f"Observed `{event.source_type}` with confidence {event.confidence:.2f}.",
            captured_at=event.observed_at,
            metadata={
                "source_type": event.source_type,
                "source_ref": event.source_ref,
                "motion_id": event.extracted_motion_id,
                "confidence": round(float(event.confidence), 2),
            },
        )

    def _receipt_source(self, payload: dict[str, Any]) -> RevOpsChatSource:
        changes = payload.get("changes") if isinstance(payload.get("changes"), list) else []
        tool_names = _unique_strings(
            str(change.get("tool") or "").strip()
            for change in changes
            if isinstance(change, dict)
        )
        return RevOpsChatSource(
            source_id=str(payload.get("run_id") or "receipt"),
            source_type="receipt",
            title=f"Receipt {payload.get('run_id') or 'unknown'}",
            summary=(
                f"Outcome `{payload.get('outcome') or 'unknown'}` with {len(changes)} change record(s)."
            ),
            captured_at=_captured_at(payload),
            metadata={
                "run_id": payload.get("run_id"),
                "outcome": payload.get("outcome"),
                "change_count": len(changes),
                "tools": tool_names[:4],
                "summary": payload.get("summary"),
            },
        )

    def _scout_evidence_preview(self, finding: dict[str, Any]) -> str:
        evidence = finding.get("evidence") if isinstance(finding.get("evidence"), list) else []
        preview: list[str] = []
        for raw in evidence[:3]:
            if not isinstance(raw, dict):
                continue
            record_id = str(raw.get("record_id") or raw.get("id") or "").strip()
            reason = str(raw.get("reason") or raw.get("reason_code") or "").strip()
            if record_id and reason:
                preview.append(f"{record_id} ({reason})")
            elif record_id:
                preview.append(record_id)
            elif reason:
                preview.append(reason)
        return "; ".join(preview)

    def _insufficient_evidence(
        self,
        *,
        answer: str,
        confidence: float,
        sources: list[RevOpsChatSource] | None = None,
        tools_used: list[str] | None = None,
        budget_used: RevOpsBudgetUsed | None = None,
        follow_ups: list[str] | None = None,
    ) -> RevOpsChatResponse:
        return RevOpsChatResponse(
            status="insufficient_evidence",
            answer=answer,
            confidence=_clamp_confidence(confidence, floor=0.0),
            sources=sources or [],
            tools_used=_unique_strings(tools_used or []),
            budget_used=budget_used or RevOpsBudgetUsed(),
            follow_up_suggestions=follow_ups or self._default_follow_ups(),
        )

    def _default_follow_ups(self) -> list[str]:
        return [
            "Which scouts found issues today?",
            "What did auto-pause do this week?",
            "Which renewals are due this month with no activity?",
            "Show me recent DNC violations.",
            "Which opportunities are not quote-ready right now?",
            "Why is opportunity 006XXXXXXXXXXXXXXX not quote-ready?",
            "Show me recent deal changes.",
            "What changed on deal 006XXXXXXXXXXXXXXX recently?",
        ]

    def _context_readiness_snapshot(self, context: DealContextV1) -> tuple[bool, list[str], list[str]]:
        open_assumptions = [
            assumption
            for assumption in context.assumptions
            if assumption.status != "confirmed" or assumption.value in (None, "", [], {})
        ]
        blockers = [_humanize_token(assumption.key) for assumption in open_assumptions]
        close_findings = [_humanize_token(value) for value in context.close_findings if str(value).strip()]
        ready = context.lifecycle_state in _READY_LIFECYCLE_STATES and not blockers and not close_findings
        return ready, blockers, close_findings

    def _resolve_conversation_scope(self, conversation_id: str | None) -> _ConversationScope:
        cleaned = _sanitize_conversation_id(conversation_id)
        if not cleaned:
            cleaned = f"revops-{uuid4().hex[:12]}"
        payload = _load_json_dict(self._conversation_path(cleaned)) or {}
        raw_last_sources = payload.get("last_sources") if isinstance(payload.get("last_sources"), list) else []
        last_sources: list[RevOpsChatSource] = []
        for raw in raw_last_sources[:6]:
            if not isinstance(raw, dict):
                continue
            try:
                last_sources.append(RevOpsChatSource.model_validate(raw))
            except Exception:
                continue
        return _ConversationScope(
            conversation_id=cleaned,
            last_intent=str(payload.get("last_intent") or "").strip() or None,
            last_scout_name=str(payload.get("last_scout_name") or "").strip() or None,
            last_opportunity_id=str(payload.get("last_opportunity_id") or "").strip() or None,
            last_module_id=str(payload.get("last_module_id") or "").strip() or None,
            last_org_alias=str(payload.get("last_org_alias") or "").strip() or None,
            last_summary_intent=str(payload.get("last_summary_intent") or "").strip() or None,
            last_summary_window=str(payload.get("last_summary_window") or "").strip() or None,
            last_summary_org_alias=str(payload.get("last_summary_org_alias") or "").strip() or None,
            last_grounded_scout_name=str(payload.get("last_grounded_scout_name") or "").strip() or None,
            last_grounded_run_id=str(payload.get("last_grounded_run_id") or "").strip() or None,
            last_grounded_finding_index=_coerce_int(payload.get("last_grounded_finding_index"), -1)
            if payload.get("last_grounded_finding_index") is not None
            else None,
            last_response_sources=tuple(last_sources),
            last_proposal_id=str(payload.get("last_proposal_id") or "").strip() or None,
            last_proposal_action_key=str(payload.get("last_proposal_action_key") or "").strip() or None,
            last_proposal_risk_tier=str(payload.get("last_proposal_risk_tier") or "").strip() or None,
            last_proposal_pending_run_id=str(payload.get("last_proposal_pending_run_id") or "").strip() or None,
            last_proposal_approval_path=str(payload.get("last_proposal_approval_path") or "").strip() or None,
        )

    def _conversation_path(self, conversation_id: str) -> Path:
        return self.state_dir / "revops_chat" / "conversations" / f"{conversation_id}.json"

    def _finalize_response(
        self,
        *,
        question: str,
        parsed: _QuestionIntent,
        response: RevOpsChatResponse,
        conversation: _ConversationScope,
        org_alias: str | None,
    ) -> RevOpsChatResponse:
        finalized = response.model_copy(
            update={
                "conversation_id": conversation.conversation_id,
                "sources": self._rank_sources(response.sources),
            }
        )
        self._persist_turn(
            question=question,
            parsed=parsed,
            response=finalized,
            conversation=conversation,
            org_alias=org_alias,
        )
        return finalized

    def _persist_turn(
        self,
        *,
        question: str,
        parsed: _QuestionIntent,
        response: RevOpsChatResponse,
        conversation: _ConversationScope,
        org_alias: str | None,
    ) -> None:
        path = self._conversation_path(conversation.conversation_id)
        path.parent.mkdir(parents=True, exist_ok=True)
        payload = _load_json_dict(path) or {}
        turns = payload.get("turns") if isinstance(payload.get("turns"), list) else []
        now = _utcnow().isoformat()
        grounded_context = self._grounded_context_from_response(parsed=parsed, response=response)
        proposal_payload = response.proposal
        summary_context = self._summary_context_from_turn(parsed=parsed, question=question, org_alias=org_alias)
        turns.append(
            {
                "asked_at": now,
                "message": question,
                "intent": parsed.intent,
                "opportunity_id": parsed.opportunity_id,
                "action_key": parsed.action_key,
                "module_id": parsed.module_id,
                "summary_window": summary_context["window_label"],
                "org_alias": org_alias,
                "status": response.status,
                "source_types": [source.source_type for source in response.sources[:6]],
                "proposal_id": proposal_payload.proposal_id if proposal_payload is not None else None,
            }
        )
        snapshot = {
            "conversation_id": conversation.conversation_id,
            "created_at": payload.get("created_at") or now,
            "updated_at": now,
            "last_intent": parsed.intent,
            "last_scout_name": parsed.scout_name or payload.get("last_scout_name"),
            "last_opportunity_id": parsed.opportunity_id or payload.get("last_opportunity_id"),
            "last_module_id": summary_context["module_id"] or payload.get("last_module_id"),
            "last_org_alias": org_alias or payload.get("last_org_alias"),
            "last_summary_intent": summary_context["intent"] or payload.get("last_summary_intent"),
            "last_summary_window": summary_context["window_label"] or payload.get("last_summary_window"),
            "last_summary_org_alias": summary_context["org_alias"] or payload.get("last_summary_org_alias"),
            "last_grounded_scout_name": grounded_context["scout_name"] or payload.get("last_grounded_scout_name"),
            "last_grounded_run_id": grounded_context["run_id"] or payload.get("last_grounded_run_id"),
            "last_grounded_finding_index": grounded_context["finding_index"]
            if grounded_context["finding_index"] is not None
            else payload.get("last_grounded_finding_index"),
            "last_sources": [source.model_dump(exclude_none=False) for source in response.sources[:6]],
            "last_proposal_id": proposal_payload.proposal_id if proposal_payload is not None else payload.get("last_proposal_id"),
            "last_proposal_action_key": proposal_payload.action_key if proposal_payload is not None else payload.get("last_proposal_action_key"),
            "last_proposal_risk_tier": proposal_payload.risk_tier if proposal_payload is not None else payload.get("last_proposal_risk_tier"),
            "last_proposal_pending_run_id": proposal_payload.pending_run_id if proposal_payload is not None else payload.get("last_proposal_pending_run_id"),
            "last_proposal_approval_path": proposal_payload.approval_path if proposal_payload is not None else payload.get("last_proposal_approval_path"),
            "turns": turns[-50:],
        }
        path.write_text(json.dumps(snapshot, indent=2), encoding="utf-8")

    def _summary_context_from_turn(
        self,
        *,
        parsed: _QuestionIntent,
        question: str,
        org_alias: str | None,
    ) -> dict[str, str | None]:
        if parsed.intent not in {"scout_summary", "module_summary"}:
            return {"intent": None, "module_id": None, "window_label": None, "org_alias": None}
        window = self._resolve_summary_window(question, fallback_label=parsed.summary_window_label)
        return {
            "intent": parsed.intent,
            "module_id": parsed.module_id,
            "window_label": window.label,
            "org_alias": org_alias,
        }

    def _grounded_context_from_response(
        self,
        *,
        parsed: _QuestionIntent,
        response: RevOpsChatResponse,
    ) -> dict[str, Any]:
        if response.status != "grounded" or not parsed.scout_name:
            return {"scout_name": None, "run_id": None, "finding_index": None}
        for source in response.sources:
            metadata = source.metadata if isinstance(source.metadata, dict) else {}
            run_id = str(metadata.get("run_id") or "").strip()
            finding_index = metadata.get("finding_index")
            if not run_id or finding_index is None:
                continue
            return {
                "scout_name": parsed.scout_name,
                "run_id": run_id,
                "finding_index": _coerce_int(finding_index, 0),
            }
        return {"scout_name": None, "run_id": None, "finding_index": None}

    def _quote_readiness_conflict_notes(
        self,
        *,
        context: DealContextV1,
        ready: bool,
        latest_receipt: dict[str, Any] | None,
        latest_event: PatternUpdateEventV1 | None,
    ) -> list[str]:
        notes: list[str] = []
        if latest_receipt is not None:
            outcome = str(latest_receipt.get("outcome") or "unknown").strip().lower() or "unknown"
            if ready and outcome not in _SUCCESS_RECEIPT_OUTCOMES:
                notes.append(
                    f"Latest receipt outcome is `{outcome}`, so the ready state may be stale."
                )
        if latest_event is not None:
            current_motion = str(context.inferred_motion_id or "").strip()
            latest_motion = str(latest_event.extracted_motion_id or "").strip()
            if current_motion and latest_motion and current_motion != latest_motion:
                notes.append(
                    f"Latest motion evidence points to `{latest_motion}` instead of `{current_motion}`."
                )
        return notes

    def _rank_sources(self, sources: list[RevOpsChatSource]) -> list[RevOpsChatSource]:
        ranked: list[tuple[int, float, int, RevOpsChatSource]] = []
        for index, source in enumerate(sources):
            metadata = source.metadata if isinstance(source.metadata, dict) else {}
            score = _SOURCE_TYPE_RANKS.get(source.source_type, 100)
            if metadata.get("conflict_note"):
                score += 80
            if metadata.get("staleness_note"):
                score += 20
            ranked.append((score, _sort_timestamp(source.captured_at), -index, source))
        ranked.sort(reverse=True)
        return [source for _, _, _, source in ranked]

    def _build_scout_sources(
        self,
        scan_payload: dict[str, Any],
        finding: dict[str, Any],
        finding_index: int,
    ) -> list[RevOpsChatSource]:
        scout_name = str(finding.get("scout_name") or "").strip() or "unknown"
        display_name = _humanize_token(scout_name)
        captured_at = _captured_at(scan_payload)
        findings_count = _coerce_int(finding.get("findings_count"), 0)
        records_scanned = _coerce_int(finding.get("total_records_scanned"), 0)
        severity = str(finding.get("severity") or "").strip().lower() or "medium"
        status = str(finding.get("status") or "unknown").strip().lower() or "unknown"
        preview = self._scout_evidence_preview(finding)
        return [
            RevOpsChatSource(
                source_id=f"{scan_payload.get('run_id') or 'scan'}:{scout_name}",
                source_type="scout_finding",
                title=f"{display_name} finding snapshot",
                summary=f"{findings_count} finding(s), status `{status}`, severity `{severity}`.",
                captured_at=captured_at,
                metadata={
                    "scout_name": scout_name,
                    "run_id": scan_payload.get("run_id"),
                    "finding_index": finding_index,
                    "findings_count": findings_count,
                    "records_scanned": records_scanned,
                    "evidence_preview": preview.split("; ") if preview else [],
                },
            ),
            RevOpsChatSource(
                source_id=str(scan_payload.get("run_id") or scout_name),
                source_type="scout_run",
                title=f"Scout run {scan_payload.get('run_id') or 'unknown'}",
                summary="Stored scout run backing the grounded scout finding.",
                captured_at=captured_at,
                metadata={
                    "run_id": scan_payload.get("run_id"),
                    "finding_index": finding_index,
                    "started_at": scan_payload.get("started_at"),
                    "ended_at": scan_payload.get("ended_at"),
                    "org_alias": scan_payload.get("org_alias"),
                },
            ),
        ]

    def _build_scout_evidence_rows(
        self,
        *,
        proposal_id: str,
        evidence_ids: list[str],
        scan_payload: dict[str, Any],
        finding_index: int,
        scout_name: str,
        rows: list[dict[str, Any]],
    ) -> list[EvidenceV1]:
        captured_at = _captured_at(scan_payload) or datetime.now(timezone.utc).isoformat()
        run_id = str(scan_payload.get("run_id") or "").strip() or "scan"
        evidence_rows: list[EvidenceV1] = []
        for evidence_id, row_index, row in zip(evidence_ids, range(len(rows)), rows):
            record_id = _row_record_id(row) or ""
            if not record_id:
                continue
            redaction_class: Literal["none", "partial", "masked"] = "partial" if row.get("fields") else "none"
            evidence_rows.append(
                EvidenceV1(
                    evidence_id=evidence_id,
                    proposal_id=proposal_id,
                    source_type="scout_finding",
                    source_ref=f"receipts/{run_id}/scan.json#finding:{finding_index}:evidence:{row_index}",
                    content_hash=_stable_content_hash(row),
                    excerpt=_scout_row_excerpt(row),
                    selector={
                        "entity_id": record_id,
                        "entity_type": str(row.get("sobject") or "").strip() or None,
                        "scout_name": scout_name,
                    },
                    captured_at=captured_at,
                    provenance_chain=[f"scan:{run_id}", f"finding:{finding_index}"],
                    trust_score=0.88,
                    snapshot_id=run_id,
                    artifact_version=str(scan_payload.get("schema_version") or "1"),
                    redaction_class=redaction_class,
                )
            )
        return evidence_rows

    def _proposal_window_bucket(self) -> str:
        now = datetime.now(timezone.utc)
        epoch = int(now.timestamp())
        bucket = epoch - (epoch % DEFAULT_DEDUPE_WINDOW_SECONDS)
        return datetime.fromtimestamp(bucket, tz=timezone.utc).isoformat()

    def _risk_profile_for_action(self, *, action_key: str, target_count: int) -> dict[str, Any]:
        if action_key == "follow_up_enforcement":
            if target_count <= 10:
                return {
                    "tier": "low",
                    "score": 0.28,
                    "factors": ["task_create", "reversible_action", "approval_required"],
                }
            return {
                "tier": "medium",
                "score": 0.42,
                "factors": ["bulk_scope", "task_create", "approval_required"],
            }
        if action_key == "slack_routing":
            return {
                "tier": "low",
                "score": 0.22 if target_count <= 25 else 0.3,
                "factors": ["human_in_loop", "notification_only", "approval_required"],
            }
        if action_key != "sequence_pause":
            return {"tier": "low", "score": 0.2, "factors": ["chat_draft", "approval_required"]}
        if target_count <= 25:
            return {
                "tier": "low",
                "score": 0.32,
                "factors": ["reversible_action", "compliance_guardrail", "approval_required"],
            }
        return {
            "tier": "medium",
            "score": 0.46,
            "factors": ["bulk_scope", "reversible_action", "approval_required"],
        }

    def _sequence_pause_reason_code(self, *, finding: dict[str, Any], rows: list[dict[str, Any]], scout_name: str) -> str:
        recommended_actions = finding.get("recommended_actions") if isinstance(finding.get("recommended_actions"), list) else []
        for action in recommended_actions:
            if not isinstance(action, dict):
                continue
            resolved = resolve_deterministic_action_key(
                action_type=str(action.get("action_type") or "").strip(),
                scout_name=scout_name,
                action_details=action.get("details") if isinstance(action.get("details"), dict) else None,
            )
            if resolved == "sequence_pause":
                details = action.get("details") if isinstance(action.get("details"), dict) else {}
                return (
                    str(details.get("reason_code") or "").strip()
                    or ACTION_KEY_REGISTRY["sequence_pause"]["default_reason_code"]
                )
        for row in rows:
            if not isinstance(row, dict):
                continue
            reason_code = str(row.get("reason_code") or "").strip()
            if reason_code:
                return reason_code
        return ACTION_KEY_REGISTRY["sequence_pause"]["default_reason_code"]

    def _target_entity_type_for_rows(self, rows: list[dict[str, Any]]) -> str:
        for row in rows:
            if not isinstance(row, dict):
                continue
            sobject = str(row.get("sobject") or "").strip()
            if sobject:
                return sobject
        return "Lead"

    def _grounded_action_requirement_message(self, action_key: str) -> str:
        if action_key == "follow_up_enforcement":
            return (
                "I can only draft a follow-up task proposal after a grounded meeting follow-up breach "
                "investigation turn with concrete row-level evidence in this conversation."
            )
        if action_key == "slack_routing":
            return (
                "I can only draft a Slack routing proposal after a grounded field-drift or customer-guardrail "
                "investigation turn with concrete row-level evidence in this conversation."
            )
        return (
            "I can only draft a pause-sequence proposal after a grounded DNC investigation turn "
            "with concrete row-level evidence in this conversation."
        )

    def _grounded_action_target_message(self, action_key: str) -> str:
        if action_key == "follow_up_enforcement":
            return (
                "The grounded follow-up breach finding does not include concrete event IDs, "
                "so I cannot draft a follow-up task proposal safely."
            )
        if action_key == "slack_routing":
            return (
                "The grounded exception finding does not include concrete record IDs, "
                "so I cannot draft a Slack routing proposal safely."
            )
        return (
            "The grounded DNC finding does not include concrete record IDs, "
            "so I cannot draft a pause-sequence proposal safely."
        )

    def _action_follow_ups(self, action_key: str) -> list[str]:
        if action_key == "follow_up_enforcement":
            return [
                "Show me meeting follow-up breaches.",
                "Which records are affected?",
            ]
        if action_key == "slack_routing":
            return [
                "Show me field drift findings.",
                "Show me customer guardrails.",
            ]
        return [
            "Show me recent DNC violations.",
            "Which records are affected?",
        ]

    def _build_grounded_action_proposal(
        self,
        *,
        action_key: str,
        conversation: _ConversationScope,
        org_alias: str | None,
        scan_payload: dict[str, Any],
        finding: dict[str, Any],
        finding_index: int,
        scoped_rows: list[dict[str, Any]],
    ) -> tuple[ProposalV1, dict[str, Any], str, str, float] | None:
        if action_key == "follow_up_enforcement":
            return self._build_follow_up_enforcement_proposal(
                conversation=conversation,
                org_alias=org_alias,
                scan_payload=scan_payload,
                finding=finding,
                finding_index=finding_index,
                scoped_rows=scoped_rows,
            )
        if action_key == "slack_routing":
            return self._build_slack_routing_proposal(
                conversation=conversation,
                org_alias=org_alias,
                scan_payload=scan_payload,
                finding=finding,
                finding_index=finding_index,
                scoped_rows=scoped_rows,
            )
        return self._build_sequence_pause_proposal(
            conversation=conversation,
            org_alias=org_alias,
            scan_payload=scan_payload,
            finding=finding,
            finding_index=finding_index,
            scoped_rows=scoped_rows,
        )

    def _build_sequence_pause_proposal(
        self,
        *,
        conversation: _ConversationScope,
        org_alias: str | None,
        scan_payload: dict[str, Any],
        finding: dict[str, Any],
        finding_index: int,
        scoped_rows: list[dict[str, Any]],
    ) -> tuple[ProposalV1, dict[str, Any], str, str, float] | None:
        action_key = "sequence_pause"
        scout_name = str(finding.get("scout_name") or "").strip() or "do_not_contact"
        record_ids = _record_ids_from_rows(scoped_rows)
        if not record_ids:
            return None
        risk_profile = self._risk_profile_for_action(action_key=action_key, target_count=len(record_ids))
        reason_code = self._sequence_pause_reason_code(finding=finding, rows=scoped_rows, scout_name=scout_name)
        target_entity_type = self._target_entity_type_for_rows(scoped_rows)
        signature = compute_proposal_signature(
            scope=f"{scan_payload.get('run_id') or conversation.conversation_id}:{scout_name}",
            action_family=action_key,
            evidence_ids=[_stable_content_hash(row) for row in scoped_rows],
            window_bucket=self._proposal_window_bucket(),
        )
        action_summary = f"Pause active sequence enrollments for {len(record_ids)} DNC-scoped record(s)."
        proposal = ProposalV1(
            tenant_id="default",
            source_kind="hybrid",
            source_refs=[
                f"scan:{scan_payload.get('run_id') or 'scan'}:finding:{finding_index}",
                f"chat:conversation:{conversation.conversation_id}",
            ],
            title=f"Pause sequences for {len(record_ids)} DNC record(s)",
            objective="Prevent outbound touches on records grounded as do-not-contact.",
            action_candidates=[
                {
                    "candidate_id": str(uuid4()),
                    "deterministic_action_key": action_key,
                    "target_selector": {
                        "entity_type": target_entity_type.lower(),
                        "record_ids": record_ids,
                        "query": f"record_ids:{len(record_ids)}",
                    },
                    "params": {
                        "record_ids": record_ids,
                        "records": scoped_rows,
                        "reason_code": reason_code,
                        "scout_name": scout_name,
                        "org_alias": org_alias,
                    },
                    "estimated_impact": {
                        "entities": len(record_ids),
                        "benefit": "prevent DNC violations",
                    },
                }
            ],
            confidence={
                "overall": 0.9 if len(record_ids) <= 10 else 0.84,
                "model_score": 0.9 if len(record_ids) <= 10 else 0.84,
                "calibrated_score": 0.86 if len(record_ids) <= 10 else 0.8,
                "calibration_version": "deterministic-v1",
            },
            risk=risk_profile,
            evidence_ids=[str(uuid4()) for _ in scoped_rows],
            why_summary=(
                f"Grounded DNC scout evidence from run `{scan_payload.get('run_id') or 'unknown'}` "
                f"identified {len(record_ids)} concrete record(s) with active-sequence pause risk."
            ),
            proposal_signature=signature,
            dedupe_window_seconds=DEFAULT_DEDUPE_WINDOW_SECONDS,
            created_by=_DEFAULT_CHAT_PROPOSAL_ACTOR,
        )
        return (
            proposal,
            risk_profile,
            action_summary,
            (
                f"I drafted a grounded pause-sequence proposal for {len(record_ids)} DNC-scoped record(s) "
                f"from scout run `{scan_payload.get('run_id') or 'unknown'}`. "
                "Review it in Pending Approvals; chat cannot approve or execute it."
            ),
            0.88 if len(record_ids) <= 10 else 0.82,
        )

    def _build_follow_up_enforcement_proposal(
        self,
        *,
        conversation: _ConversationScope,
        org_alias: str | None,
        scan_payload: dict[str, Any],
        finding: dict[str, Any],
        finding_index: int,
        scoped_rows: list[dict[str, Any]],
    ) -> tuple[ProposalV1, dict[str, Any], str, str, float] | None:
        action_key = "follow_up_enforcement"
        scout_name = str(finding.get("scout_name") or "").strip() or "meeting_followup_breach"
        record_ids = _record_ids_from_rows(scoped_rows)
        if not record_ids:
            return None
        matched_actions = self._matching_recommended_actions(
            finding=finding,
            action_key=action_key,
            scout_name=scout_name,
        )
        action_candidates = self._follow_up_enforcement_candidates(
            scoped_rows=scoped_rows,
            matched_actions=matched_actions,
            org_alias=org_alias,
        )
        if not action_candidates:
            return None
        risk_profile = self._risk_profile_for_action(action_key=action_key, target_count=len(record_ids))
        signature = compute_proposal_signature(
            scope=f"{scan_payload.get('run_id') or conversation.conversation_id}:{scout_name}",
            action_family=action_key,
            evidence_ids=[_stable_content_hash(row) for row in scoped_rows],
            window_bucket=self._proposal_window_bucket(),
        )
        action_summary = f"Create grounded follow-up tasks for {len(record_ids)} overdue meeting record(s)."
        proposal = ProposalV1(
            tenant_id="default",
            source_kind="hybrid",
            source_refs=[
                f"scan:{scan_payload.get('run_id') or 'scan'}:finding:{finding_index}",
                f"chat:conversation:{conversation.conversation_id}",
            ],
            title=f"Create follow-up tasks for {len(record_ids)} overdue meeting(s)",
            objective="Create deterministic follow-up tasks for grounded meeting follow-up breaches.",
            action_candidates=action_candidates,
            confidence={
                "overall": 0.89 if len(record_ids) <= 10 else 0.83,
                "model_score": 0.89 if len(record_ids) <= 10 else 0.83,
                "calibrated_score": 0.85 if len(record_ids) <= 10 else 0.79,
                "calibration_version": "deterministic-v1",
            },
            risk=risk_profile,
            evidence_ids=[str(uuid4()) for _ in scoped_rows],
            why_summary=(
                f"Grounded meeting follow-up breach evidence from run `{scan_payload.get('run_id') or 'unknown'}` "
                f"identified {len(record_ids)} concrete meeting record(s) with overdue follow-up enforcement risk."
            ),
            proposal_signature=signature,
            dedupe_window_seconds=DEFAULT_DEDUPE_WINDOW_SECONDS,
            created_by=_DEFAULT_CHAT_PROPOSAL_ACTOR,
        )
        return (
            proposal,
            risk_profile,
            action_summary,
            (
                f"I drafted a grounded follow-up task proposal for {len(record_ids)} overdue meeting record(s) "
                f"from scout run `{scan_payload.get('run_id') or 'unknown'}`. "
                "Review it in Pending Approvals; chat cannot approve or execute it."
            ),
            0.87 if len(record_ids) <= 10 else 0.81,
        )

    def _build_slack_routing_proposal(
        self,
        *,
        conversation: _ConversationScope,
        org_alias: str | None,
        scan_payload: dict[str, Any],
        finding: dict[str, Any],
        finding_index: int,
        scoped_rows: list[dict[str, Any]],
    ) -> tuple[ProposalV1, dict[str, Any], str, str, float] | None:
        action_key = "slack_routing"
        scout_name = str(finding.get("scout_name") or "").strip() or "customer_open_ticket_guardrail"
        record_ids = _record_ids_from_rows(scoped_rows)
        if not record_ids:
            return None
        matched_actions = self._matching_recommended_actions(
            finding=finding,
            action_key=action_key,
            scout_name=scout_name,
        )
        action_candidates = self._slack_routing_candidates(
            scout_name=scout_name,
            severity=str(finding.get("severity") or "").strip().lower() or _SLACK_ROUTING_DEFAULT_SEVERITY,
            scoped_rows=scoped_rows,
            matched_actions=matched_actions,
            org_alias=org_alias,
        )
        if not action_candidates:
            return None
        risk_profile = self._risk_profile_for_action(action_key=action_key, target_count=len(record_ids))
        signature = compute_proposal_signature(
            scope=f"{scan_payload.get('run_id') or conversation.conversation_id}:{scout_name}",
            action_family=action_key,
            evidence_ids=[_stable_content_hash(row) for row in scoped_rows],
            window_bucket=self._proposal_window_bucket(),
        )
        action_summary = f"Route {len(record_ids)} grounded exception record(s) to Slack for owner review."
        proposal = ProposalV1(
            tenant_id="default",
            source_kind="hybrid",
            source_refs=[
                f"scan:{scan_payload.get('run_id') or 'scan'}:finding:{finding_index}",
                f"chat:conversation:{conversation.conversation_id}",
            ],
            title=f"Route {len(record_ids)} grounded exception record(s) to Slack",
            objective="Route grounded field-drift or guardrail exceptions to Slack for human review.",
            action_candidates=action_candidates,
            confidence={
                "overall": 0.9 if len(record_ids) <= 10 else 0.84,
                "model_score": 0.9 if len(record_ids) <= 10 else 0.84,
                "calibrated_score": 0.86 if len(record_ids) <= 10 else 0.8,
                "calibration_version": "deterministic-v1",
            },
            risk=risk_profile,
            evidence_ids=[str(uuid4()) for _ in scoped_rows],
            why_summary=(
                f"Grounded scout evidence from run `{scan_payload.get('run_id') or 'unknown'}` "
                f"identified {len(record_ids)} concrete record(s) for deterministic Slack exception routing."
            ),
            proposal_signature=signature,
            dedupe_window_seconds=DEFAULT_DEDUPE_WINDOW_SECONDS,
            created_by=_DEFAULT_CHAT_PROPOSAL_ACTOR,
        )
        return (
            proposal,
            risk_profile,
            action_summary,
            (
                f"I drafted a grounded Slack routing proposal for {len(record_ids)} exception record(s) "
                f"from scout run `{scan_payload.get('run_id') or 'unknown'}`. "
                "Review it in Pending Approvals; chat cannot approve or execute it."
            ),
            0.89 if len(record_ids) <= 10 else 0.83,
        )

    def _matching_recommended_actions(
        self,
        *,
        finding: dict[str, Any],
        action_key: str,
        scout_name: str,
    ) -> list[dict[str, Any]]:
        recommended_actions = finding.get("recommended_actions") if isinstance(finding.get("recommended_actions"), list) else []
        matches: list[dict[str, Any]] = []
        for action in recommended_actions:
            if not isinstance(action, dict):
                continue
            details = action.get("details") if isinstance(action.get("details"), dict) else None
            resolved = resolve_deterministic_action_key(
                action_type=str(action.get("action_type") or "").strip(),
                scout_name=scout_name,
                action_details=details,
            )
            if resolved == action_key:
                matches.append(action)
        return matches

    def _follow_up_enforcement_candidates(
        self,
        *,
        scoped_rows: list[dict[str, Any]],
        matched_actions: list[dict[str, Any]],
        org_alias: str | None,
    ) -> list[dict[str, Any]]:
        row_map = {record_id: row for row in scoped_rows if (record_id := _row_record_id(row))}
        if not row_map:
            return []
        action_candidates: list[dict[str, Any]] = []
        if not matched_actions:
            matched_actions = [{"details": {}}]
        for action in matched_actions:
            details = action.get("details") if isinstance(action.get("details"), dict) else {}
            target_ids = _action_record_ids(details) or list(row_map.keys())
            candidate_rows = [row_map[record_id] for record_id in target_ids if record_id in row_map]
            if not candidate_rows:
                continue
            candidate_record_ids = _record_ids_from_rows(candidate_rows)
            if not candidate_record_ids:
                continue
            owner_ids = _unique_strings(
                [
                    str(details.get("owner_id") or "").strip(),
                    *[
                        str(row.get("fields", {}).get("OwnerId") or "").strip()
                        for row in candidate_rows
                        if isinstance(row.get("fields"), dict)
                    ],
                ]
            )
            reason_code = str(details.get("reason_code") or candidate_rows[0].get("reason_code") or "MEETING_FOLLOWUP_BREACH").strip()
            params: dict[str, Any] = {
                "record_ids": candidate_record_ids,
                "records": candidate_rows,
                "reason_code": reason_code,
                "scout_name": "meeting_followup_breach",
                "org_alias": org_alias,
                "subject": "Autopilot follow-up",
                "description": "Create follow-up tasks for grounded meeting follow-up breach evidence.",
                "apply_stage_hold": bool(details.get("apply_stage_hold")),
                "hold_stage_name": str(details.get("hold_stage_name") or "Follow-up Required").strip() or "Follow-up Required",
                "notify_slack": bool(details.get("notify_slack")),
                "slack_channel": str(details.get("slack_channel") or details.get("channel") or _SLACK_ROUTING_DEFAULT_CHANNEL).strip()
                or _SLACK_ROUTING_DEFAULT_CHANNEL,
                "slack_severity": str(details.get("slack_severity") or _SLACK_ROUTING_DEFAULT_SEVERITY).strip()
                or _SLACK_ROUTING_DEFAULT_SEVERITY,
                "action_details": details,
            }
            due_date = _follow_up_due_date(details, candidate_rows[0])
            if due_date:
                params["due_date"] = due_date
            if len(owner_ids) == 1:
                params["owner_id"] = owner_ids[0]
            event_id = str(details.get("event_id") or "").strip()
            if event_id and len(candidate_record_ids) == 1:
                params["event_id"] = event_id
            elif len(candidate_record_ids) == 1:
                params["event_id"] = candidate_record_ids[0]
            action_candidates.append(
                {
                    "candidate_id": str(uuid4()),
                    "deterministic_action_key": "follow_up_enforcement",
                    "target_selector": {
                        "entity_type": self._target_entity_type_for_rows(candidate_rows).lower(),
                        "record_ids": candidate_record_ids,
                        "query": f"record_ids:{len(candidate_record_ids)}",
                    },
                    "params": params,
                    "estimated_impact": {
                        "entities": len(candidate_record_ids),
                        "benefit": "create overdue follow-up tasks",
                    },
                }
            )
        return action_candidates

    def _slack_routing_candidates(
        self,
        *,
        scout_name: str,
        severity: str,
        scoped_rows: list[dict[str, Any]],
        matched_actions: list[dict[str, Any]],
        org_alias: str | None,
    ) -> list[dict[str, Any]]:
        row_map = {record_id: row for row in scoped_rows if (record_id := _row_record_id(row))}
        if not row_map:
            return []
        action_candidates: list[dict[str, Any]] = []
        if not matched_actions:
            grouped_rows: dict[str, list[dict[str, Any]]] = {}
            for row in scoped_rows:
                reason_code = str(row.get("reason_code") or "EXCEPTION_ROUTED").strip() or "EXCEPTION_ROUTED"
                grouped_rows.setdefault(reason_code, []).append(row)
            matched_actions = [
                {"details": {"reason_code": reason_code, "record_ids": _record_ids_from_rows(rows)}}
                for reason_code, rows in grouped_rows.items()
            ]
        for action in matched_actions:
            details = action.get("details") if isinstance(action.get("details"), dict) else {}
            target_ids = _action_record_ids(details) or list(row_map.keys())
            candidate_rows = [row_map[record_id] for record_id in target_ids if record_id in row_map]
            if not candidate_rows:
                continue
            candidate_record_ids = _record_ids_from_rows(candidate_rows)
            if not candidate_record_ids:
                continue
            reason_code = str(details.get("reason_code") or candidate_rows[0].get("reason_code") or "EXCEPTION_ROUTED").strip() or "EXCEPTION_ROUTED"
            params = {
                "record_ids": candidate_record_ids,
                "records": candidate_rows,
                "reason_code": reason_code,
                "scout_name": scout_name,
                "org_alias": org_alias,
                "channel": str(details.get("channel") or _SLACK_ROUTING_DEFAULT_CHANNEL).strip() or _SLACK_ROUTING_DEFAULT_CHANNEL,
                "severity": str(details.get("severity") or severity or _SLACK_ROUTING_DEFAULT_SEVERITY).strip() or _SLACK_ROUTING_DEFAULT_SEVERITY,
                "title": str(details.get("title") or f"{_SLACK_ROUTING_TITLE_PREFIX}: {reason_code}").strip()
                or f"{_SLACK_ROUTING_TITLE_PREFIX}: {reason_code}",
                "message": str(details.get("message") or self._slack_routing_message(scout_name=scout_name, reason_code=reason_code, rows=candidate_rows)).strip(),
                "action_details": details,
            }
            action_candidates.append(
                {
                    "candidate_id": str(uuid4()),
                    "deterministic_action_key": "slack_routing",
                    "target_selector": {
                        "entity_type": self._target_entity_type_for_rows(candidate_rows).lower(),
                        "record_ids": candidate_record_ids,
                        "query": f"record_ids:{len(candidate_record_ids)}",
                    },
                    "params": params,
                    "estimated_impact": {
                        "entities": len(candidate_record_ids),
                        "benefit": "route exceptions for human review",
                    },
                }
            )
        return action_candidates

    def _slack_routing_message(self, *, scout_name: str, reason_code: str, rows: list[dict[str, Any]]) -> str:
        return f"{scout_name} flagged {len(rows)} record(s) for `{reason_code}` and routed them for human review."


def _utcnow() -> datetime:
    return datetime.now(timezone.utc)


def _severity_rank(value: str | None) -> int:
    return _SEVERITY_RANKS.get(str(value or "").strip().lower() or "unknown", 0)


def _receipt_module_ids(payload: dict[str, Any]) -> set[str]:
    module_ids: set[str] = set()
    raw_module_tags = payload.get("module_tags")
    if isinstance(raw_module_tags, list):
        for raw in raw_module_tags:
            module_id = str(raw or "").strip()
            if module_id:
                module_ids.add(module_id)
    if module_ids:
        return module_ids

    changes = payload.get("changes")
    if not isinstance(changes, list):
        return set()
    for change in changes:
        if not isinstance(change, dict):
            continue
        tool_name = str(change.get("tool") or "").strip()
        if not tool_name:
            continue
        module_id, _write_class = infer_module_write_class_from_tool(tool_name)
        if module_id:
            module_ids.add(module_id)
    return module_ids


def _receipt_change_count(payload: dict[str, Any]) -> int:
    changes = payload.get("changes")
    if not isinstance(changes, list):
        return 0
    return len([change for change in changes if isinstance(change, dict)])


def _format_summary_counts(counts: dict[str, int]) -> str:
    if not counts:
        return "no recorded outcomes"
    return ", ".join(
        f"`{label}` ({count})"
        for label, count in sorted(counts.items(), key=lambda item: (-int(item[1]), item[0]))
    )


def _module_summary_display_name(module_id: str) -> str:
    return _MODULE_SUMMARY_DISPLAY_NAMES.get(module_id, _humanize_token(module_id))


def _detect_summary_window_label(message: str) -> str | None:
    lowered = _normalize_message_text(message)
    if "last 24 hours" in lowered:
        return "the last 24 hours"
    if "last 7 days" in lowered or "past 7 days" in lowered:
        return "the last 7 days"
    if "last week" in lowered:
        return "last week"
    if "this month" in lowered:
        return "this month"
    if "this week" in lowered:
        return "this week"
    if "today" in lowered:
        return "today"
    return None


def _summary_window_from_label(label: str) -> _SummaryWindow:
    normalized = str(label or "").strip().lower() or "the last 7 days"
    now = _utcnow()
    day_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
    current_week_start = day_start - timedelta(days=day_start.weekday())
    if normalized == "today":
        return _SummaryWindow(label="today", start_at=day_start, end_at=day_start + timedelta(days=1))
    if normalized == "this week":
        return _SummaryWindow(label="this week", start_at=current_week_start)
    if normalized == "last week":
        return _SummaryWindow(
            label="last week",
            start_at=current_week_start - timedelta(days=7),
            end_at=current_week_start,
        )
    if normalized == "this month":
        return _SummaryWindow(label="this month", start_at=day_start.replace(day=1))
    if normalized == "the last 24 hours":
        return _SummaryWindow(label="the last 24 hours", start_at=now - timedelta(hours=24))
    return _SummaryWindow(label="the last 7 days", start_at=now - timedelta(days=7))


def _summary_window_contains(window: _SummaryWindow, timestamp: datetime) -> bool:
    if timestamp < window.start_at:
        return False
    if window.end_at is not None and timestamp >= window.end_at:
        return False
    return True


def _load_json_dict(path: Path) -> dict[str, Any] | None:
    if not path.exists():
        return None
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return None
    return payload if isinstance(payload, dict) else None


def _normalize_message_text(message: str) -> str:
    text = str(message or "").lower().replace("\u2019", "'")
    return " ".join(text.split())


def _weighted_phrase_matches(
    message: str,
    phrases: tuple[tuple[str, float], ...],
) -> tuple[list[str], float]:
    hits: list[str] = []
    seen: set[str] = set()
    score = 0.0
    for phrase, weight in phrases:
        if phrase not in message or phrase in seen:
            continue
        seen.add(phrase)
        hits.append(phrase)
        score += weight
    return hits, score


def _contains_phrase(message: str, phrases: tuple[str, ...]) -> bool:
    return any(phrase in message for phrase in phrases)


def _looks_like_overview_request(message: str) -> bool:
    lowered = _normalize_message_text(message)
    if any(phrase in lowered for phrase in _OVERVIEW_REQUEST_PHRASES):
        return True
    return "opportunities" in lowered or "deals" in lowered


def _looks_like_specific_entity_token(value: str) -> bool:
    text = str(value or "").strip()
    if not text:
        return False
    if any(char.isdigit() for char in text):
        return True
    return any(char in "._:-" for char in text)


def _scan_sort_key(payload: dict[str, Any]) -> float:
    return _sort_timestamp(str(payload.get("ended_at") or payload.get("started_at") or ""))


def _receipt_sort_key(payload: dict[str, Any]) -> float:
    return _sort_timestamp(str(payload.get("completed_at") or payload.get("started_at") or ""))


def _sort_timestamp(value: str | None) -> float:
    parsed = _parse_iso(value)
    return parsed.timestamp() if parsed is not None else 0.0


def _parse_iso(value: str | None) -> datetime | None:
    raw = str(value or "").strip()
    if not raw:
        return None
    try:
        normalized = raw.replace("Z", "+00:00")
        parsed = datetime.fromisoformat(normalized)
    except Exception:
        return None
    if parsed.tzinfo is None:
        return parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _coerce_int(value: Any, default: int = 0) -> int:
    try:
        return int(value)
    except Exception:
        return default


def _captured_at(payload: dict[str, Any]) -> str | None:
    return str(payload.get("ended_at") or payload.get("completed_at") or payload.get("started_at") or "").strip() or None


def _receipt_opportunity_targets(payload: dict[str, Any]) -> list[str]:
    changes = payload.get("changes")
    if not isinstance(changes, list):
        return []
    targets: list[str] = []
    for change in changes:
        if not isinstance(change, dict):
            continue
        raw_targets = change.get("targets")
        if not isinstance(raw_targets, list):
            continue
        targets.extend(str(target).strip() for target in raw_targets if str(target).strip())
    return _unique_strings(targets)


def _receipt_targets_opportunity(payload: dict[str, Any], opportunity_id: str) -> bool:
    return opportunity_id in _receipt_opportunity_targets(payload)


def _humanize_token(value: str | None) -> str:
    text = str(value or "").strip()
    if not text:
        return ""
    return text.replace("_", " ").replace("-", " ")


def _at_time_suffix(value: str | None) -> str:
    return f" at {value}" if value else ""


def _clamp_confidence(value: float, *, floor: float) -> float:
    parsed = float(value)
    if parsed < floor:
        return floor
    if parsed > 1.0:
        return 1.0
    return parsed


def _unique_strings(values: list[str] | tuple[str, ...] | Any) -> list[str]:
    ordered: list[str] = []
    seen: set[str] = set()
    for raw in values:
        cleaned = str(raw or "").strip()
        if not cleaned or cleaned in seen:
            continue
        seen.add(cleaned)
        ordered.append(cleaned)
    return ordered


def _sanitize_conversation_id(value: str | None) -> str | None:
    cleaned = re.sub(r"[^A-Za-z0-9._-]", "", str(value or "").strip())
    return cleaned or None


def _stable_content_hash(payload: Any) -> str:
    digest = hashlib.sha256(
        json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
    ).hexdigest()
    return f"sha256:{digest}"


def _row_record_id(row: dict[str, Any]) -> str | None:
    record_id = str(row.get("record_id") or row.get("id") or "").strip()
    return record_id or None


def _record_ids_from_rows(rows: list[dict[str, Any]]) -> list[str]:
    return _unique_strings([record_id for row in rows if isinstance(row, dict) and (record_id := _row_record_id(row))])


def _action_record_ids(details: dict[str, Any]) -> list[str]:
    raw_ids = details.get("record_ids") if isinstance(details.get("record_ids"), list) else []
    candidates = [str(value or "").strip() for value in raw_ids]
    for key in ("event_id", "record_id", "entity_id"):
        value = str(details.get(key) or "").strip()
        if value:
            candidates.append(value)
    return _unique_strings(candidates)


def _follow_up_due_date(details: dict[str, Any], row: dict[str, Any]) -> str | None:
    due_date = str(details.get("due_date") or "").strip()
    if due_date:
        return due_date[:10]
    due_at = str(details.get("due_at") or "").strip()
    if due_at:
        return due_at[:10]
    fields = row.get("fields") if isinstance(row.get("fields"), dict) else {}
    deadline = str(fields.get("followup_deadline") or "").strip()
    if deadline:
        return deadline[:10]
    return None


def _scout_row_excerpt(row: dict[str, Any]) -> str:
    record_id = _row_record_id(row) or "unknown"
    reason = str(row.get("reason") or row.get("reason_code") or "Scout evidence").strip() or "Scout evidence"
    return f"{record_id}: {reason}"[:280]


__all__ = [
    "RevOpsBudgetUsed",
    "RevOpsChatProposalPayload",
    "RevOpsChatRequest",
    "RevOpsChatResponse",
    "RevOpsChatService",
    "RevOpsChatSource",
]
