"""
Pydantic Models for Echo Ops Endpoints

WO-ECHO-TEAMMATE-01: Data models for /echo/brief, /echo/act, /echo/triage
WO-ONTOLOGY-HARDENING-01: Data models for /echo/map
"""

from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Literal, Optional

from pydantic import BaseModel, Field


# ============================================================================
# /echo/brief - Daily Standup Models
# ============================================================================

class HealthLevel(str, Enum):
    """Health signal levels."""
    RED = "red"
    YELLOW = "yellow"
    GREEN = "green"


class RedIssue(BaseModel):
    """Critical issue requiring immediate attention."""
    issue_type: Literal["failing_tests", "crashing_service", "broken_import"]
    description: str
    source: str  # File/test/module path
    details: Optional[Dict[str, Any]] = None
    first_seen: Optional[str] = None


class YellowIssue(BaseModel):
    """Warning issue requiring attention."""
    issue_type: Literal["flaky_bench", "todo_hotspot", "half_integrated", "stale_receipt"]
    description: str
    source: str
    details: Optional[Dict[str, Any]] = None


class GreenItem(BaseModel):
    """Healthy signal confirming good state."""
    item_type: Literal["receipt", "commit", "healthcheck", "passing_test"]
    description: str
    source: str
    timestamp: Optional[str] = None


class NextAction(BaseModel):
    """Recommended next action with priority and command."""
    priority: int = Field(..., ge=1, le=5, description="1=highest, 5=lowest")
    description: str
    command: Optional[str] = None  # Exact shell command to run
    rationale: Optional[str] = None


class SeMetrics(BaseModel):
    """Symbolic Entropy metrics for cognitive integration."""
    H: float = Field(..., description="Entropy component (0-1)")
    C: float = Field(..., description="Coherence component (0-1)")
    D: float = Field(..., description="Depth component (0-1)")
    Se: float = Field(..., description="Symbolic Entropy = H × C × D")
    regime: str = Field(..., description="low_se_structured, mid_se_balanced, or high_se_exploratory")
    classification_mode: str = Field(
        ...,
        description="Routing mode: analytical (LEFT), intuitive (RIGHT), or creative (POET)"
    )


class BriefResponse(BaseModel):
    """Response for /echo/brief - Daily standup."""
    timestamp: str
    health_level: HealthLevel
    red: List[RedIssue] = Field(default_factory=list)
    yellow: List[YellowIssue] = Field(default_factory=list)
    green: List[GreenItem] = Field(default_factory=list)
    next_actions: List[NextAction] = Field(default_factory=list)
    summary: str
    data_sources: Dict[str, Any] = Field(
        default_factory=dict,
        description="Metadata about data sources used"
    )
    # Se integration (WO-ECHO-TEAMMATE-02)
    se_metrics: Optional[SeMetrics] = Field(
        default=None,
        description="Symbolic Entropy metrics for cognitive integration"
    )
    phase_lock_id: Optional[int] = Field(
        default=None,
        description="EchoMemory phase-lock ID for Se continuity"
    )


# ============================================================================
# /echo/act - Safe Tool Runner Models
# ============================================================================

class ActRequest(BaseModel):
    """Request for /echo/act - Safe tool runner."""
    command: str = Field(..., description="Base command (must be in allowlist)")
    args: List[str] = Field(default_factory=list, description="Command arguments")
    timeout_seconds: int = Field(default=60, ge=1, le=600)
    dry_run: bool = Field(default=False, description="If true, validate without executing")
    cwd: Optional[str] = Field(
        default=None,
        description="Working directory (must be within project root)"
    )


class ActReceipt(BaseModel):
    """Execution receipt for /echo/act."""
    receipt_id: str
    timestamp: str
    command: str
    args: List[str]
    exit_code: int
    stdout: str
    stderr: str
    elapsed_seconds: float
    receipt_path: str


class ActResponse(BaseModel):
    """Response for /echo/act."""
    status: Literal["success", "error", "blocked", "dry_run"]
    message: str
    receipt: Optional[ActReceipt] = None
    blocked_reason: Optional[str] = None
    validated_command: Optional[str] = None  # Full command that would/did run


# ============================================================================
# /echo/triage - Idea/Stub Classification Models
# ============================================================================

class TriageRequest(BaseModel):
    """Request for /echo/triage - Idea/stub classification."""
    target: str = Field(
        ...,
        description="Folder path, module name, or 'WO-XXX' work order reference"
    )
    include_dependencies: bool = Field(
        default=True,
        description="Include import fan-in/fan-out analysis"
    )
    max_depth: int = Field(default=3, ge=1, le=10)


class TriageCategory(str, Enum):
    """Classification categories."""
    ALIVE = "alive"      # Evidence >=2, connectivity >=2, status PASS
    DEAD = "dead"        # Status FAIL/DISPROVEN
    CONTINUE = "continue"  # Connectivity >=2, effort >=2, low evidence
    FREEZE = "freeze"    # Low connectivity, low evidence


class RubricScore(BaseModel):
    """Rubric scores (0-3 each)."""
    evidence: int = Field(..., ge=0, le=3, description="Receipt/benchmark count")
    connectivity: int = Field(..., ge=0, le=3, description="Import fan-in/fan-out")
    effort: int = Field(..., ge=0, le=3, description="Inverted: lines of code")
    risk: int = Field(..., ge=0, le=3, description="Inverted: TODO count, no receipts")


class TriagedItem(BaseModel):
    """A triaged module/file/work order."""
    name: str
    path: Optional[str] = None
    category: TriageCategory
    rubric: RubricScore
    reasons: List[str] = Field(default_factory=list)
    receipts: List[str] = Field(default_factory=list)  # Paths to related receipts
    todos: int = 0
    imports_in: List[str] = Field(default_factory=list)  # Files that import this
    imports_out: List[str] = Field(default_factory=list)  # Files this imports
    lines_of_code: int = 0


class TriageResponse(BaseModel):
    """Response for /echo/triage."""
    timestamp: str
    target: str
    alive: List[TriagedItem] = Field(default_factory=list)
    dead: List[TriagedItem] = Field(default_factory=list)
    stubs_continue: List[TriagedItem] = Field(default_factory=list)
    stubs_freeze: List[TriagedItem] = Field(default_factory=list)
    summary: str
    scan_metadata: Dict[str, Any] = Field(default_factory=dict)
    # Se integration (WO-ECHO-TEAMMATE-02)
    se_metrics: Optional[SeMetrics] = Field(
        default=None,
        description="Se metrics for target module"
    )
    classification_mode: Optional[str] = Field(
        default=None,
        description="Se-based routing: analytical, intuitive, or creative"
    )


# ============================================================================
# Nightly Health Check Models
# ============================================================================

class HealthCheckResult(BaseModel):
    """Result of a single health check."""
    name: str
    status: Literal["PASS", "FAIL", "ERROR", "SKIP"]
    passed: bool
    critical: bool = True
    elapsed_seconds: float
    details: Dict[str, Any] = Field(default_factory=dict)
    error_message: Optional[str] = None


class NightlyHealthReport(BaseModel):
    """Report from nightly health scan."""
    receipt_type: str = "ECHO_NIGHTLY_HEALTH"
    work_order: str = "WO-ECHO-TEAMMATE-02"
    timestamp: str
    summary: Dict[str, Any]
    checks: List[HealthCheckResult]
    system_info: Dict[str, Any] = Field(default_factory=dict)
    # Se integration (WO-ECHO-TEAMMATE-02)
    se_metrics: Optional[SeMetrics] = Field(
        default=None,
        description="Se metrics for health state"
    )
    phase_lock_id: Optional[int] = Field(
        default=None,
        description="EchoMemory phase-lock ID"
    )


# ============================================================================
# Nightly Stubs Scan Models
# ============================================================================

class TodoMarker(BaseModel):
    """A TODO/FIXME/XXX/HACK marker in code."""
    marker_type: Literal["TODO", "FIXME", "XXX", "HACK"]
    text: str
    file_path: str
    line_number: int
    context: Optional[str] = None  # Surrounding code


class OrphanModule(BaseModel):
    """A module never imported elsewhere."""
    path: str
    lines_of_code: int
    has_main: bool = False
    has_tests: bool = False


class DuplicateFunction(BaseModel):
    """Functions with same name in different files."""
    function_name: str
    locations: List[str]  # file:line references


class CallgraphHotspot(BaseModel):
    """High fan-in/fan-out module."""
    path: str
    fan_in: int
    fan_out: int
    score: float  # fan_in + fan_out weighted


class NightlyStubsReport(BaseModel):
    """Report from nightly stubs scan."""
    receipt_type: str = "ECHO_NIGHTLY_STUBS"
    work_order: str = "WO-ECHO-TEAMMATE-02"
    timestamp: str
    todos: List[TodoMarker] = Field(default_factory=list)
    orphan_modules: List[OrphanModule] = Field(default_factory=list)
    duplicate_functions: List[DuplicateFunction] = Field(default_factory=list)
    callgraph_hotspots: List[CallgraphHotspot] = Field(default_factory=list)
    summary: Dict[str, Any] = Field(default_factory=dict)
    # Se integration (WO-ECHO-TEAMMATE-02)
    se_metrics: Optional[SeMetrics] = Field(
        default=None,
        description="Se metrics for stubs state"
    )
    phase_lock_id: Optional[int] = Field(
        default=None,
        description="EchoMemory phase-lock ID"
    )


# ============================================================================
# /echo/map - System Layout Models (WO-ONTOLOGY-HARDENING-01)
# ============================================================================

class GitState(BaseModel):
    """Git repository state."""
    head: Optional[str] = Field(None, description="Current HEAD commit (short hash)")
    branch: Optional[str] = Field(None, description="Current branch name")
    dirty: Optional[bool] = Field(None, description="Whether working tree has uncommitted changes")


class PortStatus(BaseModel):
    """Status of a service port."""
    port: int
    status: Literal["healthy", "not_running", "error"]
    backend: Optional[str] = Field(None, description="Backend identifier if detected")


class DecodeOnlyBackends(BaseModel):
    """Decode-only backend availability."""
    helixcode_v0: bool = Field(..., description="HelixCode v0 block decode")
    cdna_streaming: bool = Field(..., description="CDNA streaming reader")
    stream_xw_matmul: bool = Field(..., description="Stream X@W matmul primitive")


class ChatBackends(BaseModel):
    """Chat backend availability."""
    llama_cpp: bool = Field(..., description="llama.cpp Python bindings")
    basin_server: bool = Field(..., description="Basin server module")


class BackendsStatus(BaseModel):
    """All backends status."""
    decode_only: DecodeOnlyBackends
    chat: ChatBackends
    qpu_stubs: bool = Field(..., description="QPU bridge stubs available")


class TripwireDefaults(BaseModel):
    """Tripwire configuration defaults."""
    inference_tripwire_enabled: bool = Field(True, description="InferenceTripwire enabled by default")
    helix_allow_foreign_inference: str = Field(
        "",
        description="Value of HELIX_ALLOW_FOREIGN_INFERENCE env var"
    )


class ReceiptsSummary(BaseModel):
    """Summary of recent receipts."""
    last_7_days: Dict[str, int] = Field(..., description="Pass/fail counts from last 7 days")
    newest: Optional[str] = Field(None, description="Path to most recent receipt")


class ProofPointers(BaseModel):
    """
    SHA256 proof pointers for critical files.

    WO-COAUTHOR-LOOP-HARDEN-01: Make /echo/map structurally impossible to silently lie.
    Each pointer is a truncated SHA256 hash (first 16 hex chars) of a critical file.
    """
    claude_md_sha256: str = Field(..., description="SHA256 of CLAUDE.md (repo root)")
    tripwire_sha256: str = Field(..., description="SHA256 of inference_tripwire.py")
    newest_receipt_sha256: str = Field(..., description="SHA256 of most recent receipt file")
    coauthor_trace_sha256: str = Field(..., description="SHA256 of last 100 lines of coauthor_trace.jsonl")
    ontology_lint_last_run: Optional[str] = Field(None, description="ISO timestamp of last ontology lint run")
    ontology_lint_status: Optional[str] = Field(None, description="PASS or FAIL from last ontology lint")


class MapIntegrity(BaseModel):
    """
    Integrity validation of proof pointers.

    WO-COAUTHOR-LOOP-HARDEN-01: "If a sensor is unplugged, the panel screams."
    """
    integrity: Literal["ok", "degraded", "broken"] = Field(
        ...,
        description="ok = all pointers valid, degraded = 1-2 missing, broken = 3+ missing"
    )
    missing: List[str] = Field(
        default_factory=list,
        description="List of missing/invalid proof pointer keys"
    )
    valid_count: int = Field(..., description="Number of valid proof pointers")
    total_required: int = Field(..., description="Total required proof pointers")


class MapResponse(BaseModel):
    """
    Response for /echo/map - System layout.

    WO-ONTOLOGY-HARDENING-01: Observability for Echo's architecture.
    Shows "what is plugged in where" without implying self-awareness.

    WO-COAUTHOR-LOOP-HARDEN-01: Added proof_pointers for court-grade auditability.
    """
    timestamp: str = Field(..., description="ISO 8601 timestamp")
    git: GitState = Field(..., description="Git repository state")
    ports: Dict[str, PortStatus] = Field(..., description="Service port status")
    backends: BackendsStatus = Field(..., description="Backend availability")
    modules: Dict[str, str] = Field(
        ...,
        description="Module load status (loaded, import_error, error)"
    )
    tripwire_defaults: TripwireDefaults = Field(
        ...,
        description="InferenceTripwire configuration"
    )
    receipts_summary: ReceiptsSummary = Field(..., description="Recent receipts summary")
    key_paths: Dict[str, str] = Field(
        ...,
        description="Paths to key system files"
    )
    proof_pointers: Optional[ProofPointers] = Field(
        None,
        description="SHA256 proof pointers for critical files (WO-COAUTHOR-LOOP-HARDEN-01)"
    )
    map_integrity: Optional[MapIntegrity] = Field(
        None,
        description="Integrity validation - if sensors are missing, this screams (WO-COAUTHOR-LOOP-HARDEN-01)"
    )


# ============================================================================
# /echo/spatial - Spatial Memory Tool Models
# ============================================================================

class SpatialQueryType(str, Enum):
    """Spatial query types supported by the FibPi3D backend."""
    NEAR = "near"           # Is A near B? (within threshold distance)
    DISTANCE = "distance"   # Euclidean distance from A to B
    K_NEAREST = "k_nearest" # K closest objects to A
    BETWEEN = "between"     # Is B geometrically between A and C?
    INSIDE = "inside"       # Is A inside radius of B?
    LEFT_OF = "left_of"     # Is A.x < B.x?
    ABOVE = "above"         # Is A.z > B.z?


class SpatialQuery(BaseModel):
    """A single spatial query to execute."""
    query_type: SpatialQueryType = Field(..., description="Type of spatial query")
    a: str = Field(..., description="First object name (required for all queries)")
    b: Optional[str] = Field(None, description="Second object name (for binary predicates)")
    c: Optional[str] = Field(None, description="Third object name (for 'between' query)")
    k: int = Field(default=5, ge=1, le=100, description="Number of neighbors for k_nearest")
    threshold: float = Field(default=1.0, gt=0, description="Distance threshold for near/inside")
    tolerance: float = Field(default=0.3, gt=0, description="Line distance tolerance for between")


class SceneConfigRequest(BaseModel):
    """Configuration for scene generation."""
    n_objects: int = Field(default=100, ge=1, le=10000, description="Number of objects")
    distribution: str = Field(
        default="uniform",
        description="Object distribution: uniform, gaussian, or clustered"
    )
    bounds_min: float = Field(default=-10.0, description="Minimum coordinate bound")
    bounds_max: float = Field(default=10.0, description="Maximum coordinate bound")
    seed: Optional[str] = Field(None, description="Random seed (defaults to scene_id)")


class SpatialMemoryRequest(BaseModel):
    """Request for /echo/spatial endpoint."""
    scene_id: str = Field(default="default", description="Scene identifier")
    queries: List[SpatialQuery] = Field(..., min_length=1, description="Batch of spatial queries")
    create_scene_if_missing: bool = Field(
        default=True,
        description="Create scene with default config if not found"
    )
    scene_config: Optional[SceneConfigRequest] = Field(
        None,
        description="Scene configuration (used when creating)"
    )


class SpatialQueryResult(BaseModel):
    """Result of a single spatial query."""
    query_type: str = Field(..., description="Type of query executed")
    a: str = Field(..., description="First object name")
    b: Optional[str] = Field(None, description="Second object name")
    c: Optional[str] = Field(None, description="Third object name")
    result: Any = Field(..., description="Query result (bool, float, or list of tuples)")
    latency_us: float = Field(..., description="Query latency in microseconds")
    error: Optional[str] = Field(None, description="Error message if query failed")


class SpatialMemoryResponse(BaseModel):
    """Response from /echo/spatial endpoint."""
    scene_id: str = Field(..., description="Scene identifier")
    scene_hash: str = Field(..., description="SHA256 hash of scene for determinism verification")
    n_objects: int = Field(..., description="Number of objects in scene")
    results: List[SpatialQueryResult] = Field(..., description="Query results")
    total_latency_ms: float = Field(..., description="Total execution time in milliseconds")
    receipt_id: str = Field(..., description="Evidence bus receipt ID")
    se_metrics: Optional[SeMetrics] = Field(
        None,
        description="Symbolic Entropy metrics for the spatial operation"
    )


class SpatialSceneInfo(BaseModel):
    """Information about a spatial scene."""
    scene_id: str
    n_objects: int
    scene_hash: str
    distribution: str
    created_at: str


class SpatialScenesResponse(BaseModel):
    """Response for listing available scenes."""
    scenes: List[SpatialSceneInfo] = Field(default_factory=list)
    count: int = Field(..., description="Total number of scenes")


# ============================================================================
# Echo Loop Receipt Models (WO-ECHO-FIBPI3D-LOOP-01)
# ============================================================================

# Router schema version for forward compatibility (WO-MODULAR-INVENTORY-01)
ROUTER_SCHEMA_VERSION = "1.0.0"


class RouterDecision(BaseModel):
    """
    Router decision for intent detection.

    WO-MODULAR-INVENTORY-01: Extended to support multiple tool types.
    """
    # Intent flags (at least one should be True for routing)
    is_geometry: bool = Field(default=False, description="Whether geometry intent was detected")
    is_math: bool = Field(default=False, description="Whether math intent was detected")
    is_retrieval: bool = Field(default=False, description="Whether retrieval intent was detected")
    is_code: bool = Field(default=False, description="Whether code intent was detected")

    # Detection details
    matched_keywords: List[str] = Field(default_factory=list, description="Keywords that matched")
    matched_patterns: List[str] = Field(default_factory=list, description="Regex patterns that matched")
    confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score")
    suggested_tool: Optional[str] = Field(None, description="Tool to route to")

    # Schema version for forward compatibility
    router_schema_version: str = Field(
        default=ROUTER_SCHEMA_VERSION,
        description="Router schema version for forward compatibility"
    )


class EchoLoopReceipt(BaseModel):
    """
    Receipt spanning Echo->Tool->Echo loop.

    WO-ECHO-FIBPI3D-LOOP-01: Full-loop receipt chain for geometry prompts.
    WO-MODULAR-INVENTORY-01: Extended with router_schema_version for forward compatibility.
    WO-TOOL-ROUTING-HARDEN-01: Added router_decision_hash_basis for re-derivable chain hash.

    Tracks: prompt -> router decision -> tool call -> tool receipt -> final answer
    with chain hash for verification.
    """
    # Identification
    loop_id: str = Field(..., description="Unique loop identifier")
    timestamp_utc: str = Field(..., description="ISO8601 timestamp")
    work_order: str = Field(default="WO-ECHO-FIBPI3D-LOOP-01")

    # Schema version for forward compatibility (WO-MODULAR-INVENTORY-01)
    router_schema_version: str = Field(
        default=ROUTER_SCHEMA_VERSION,
        description="Router schema version for forward compatibility"
    )

    # Step 1: Input
    prompt: str = Field(..., description="Original user prompt")
    prompt_sha256: str = Field(..., description="SHA256 of prompt")

    # Step 2: Router decision
    router_decision: RouterDecision = Field(..., description="Geometry detection result")
    router_receipt_id: Optional[str] = Field(None, description="Receipt ID for routing decision")

    # WO-TOOL-ROUTING-HARDEN-01: Store raw dict for chain hash re-derivation
    # The Pydantic model (router_decision) may have extra fields; this stores
    # the exact dict used to compute chain_hash, enabling verification.
    router_decision_hash_basis: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Exact dict used for chain hash computation (for re-derivation)"
    )

    # Step 3: Tool call
    tool_endpoint: str = Field(..., description="Endpoint called (e.g., /echo/spatial)")
    tool_request: Dict[str, Any] = Field(..., description="Request sent to tool")
    tool_response: Dict[str, Any] = Field(..., description="Response from tool")
    tool_receipt_id: str = Field(..., description="Receipt ID from tool call")
    tool_scene_hash: str = Field(..., description="Scene hash for determinism verification")

    # Step 4: Final answer
    answer: str = Field(..., description="Final answer constructed from tool results")
    answer_sha256: str = Field(..., description="SHA256 of answer")

    # Chain verification
    chain_hash: str = Field(
        ...,
        description="SHA256(prompt_sha256 || router || tool_receipt_id || answer_sha256)"
    )

    # Se metadata (non-blocking, for observability)
    se_metadata: Optional[SeMetrics] = Field(
        None,
        description="Se metrics computed for this loop (logged, not gated)"
    )

    # Status
    # WO-TOOL-ROUTING-HARDEN-01: Added "UNIMPLEMENTED" for stub routes
    status: Literal["PASS", "FAIL", "UNIMPLEMENTED"] = Field(
        default="PASS",
        description="Overall loop status (UNIMPLEMENTED = stub route)"
    )
    errors: List[str] = Field(
        default_factory=list,
        description="Any errors encountered during loop"
    )


# ============================================================================
# Uncertainty Dispatch Receipt (WO-ECHO-UNCERTAINTY-DISPATCH)
# ============================================================================


class AgentResultModel(BaseModel):
    """Result from a single agent dispatch."""
    agent_id: str = Field(..., description="Unique agent execution ID")
    agent_type: str = Field(..., description="Type of agent (codebase, search, etc.)")
    query: str = Field(..., description="Query sent to agent")
    result: Optional[Any] = Field(None, description="Agent result data")
    sources: List[str] = Field(default_factory=list, description="Source references")
    confidence: float = Field(..., ge=0.0, le=1.0, description="Agent confidence in result")
    elapsed_ms: float = Field(..., description="Execution time in milliseconds")
    error: Optional[str] = Field(None, description="Error message if agent failed")


class UncertaintyDispatchReceipt(BaseModel):
    """
    Receipt for uncertainty-based agent dispatch.

    WO-ECHO-UNCERTAINTY-DISPATCH: Full-loop receipt for uncertainty queries
    that trigger agent dispatch instead of direct tool routing.

    Tracks: prompt -> uncertainty detection -> agent dispatch -> synthesis -> answer
    """
    # Identification
    receipt_id: str = Field(..., description="Unique receipt identifier")
    timestamp_utc: str = Field(..., description="ISO8601 timestamp")
    work_order: str = Field(default="WO-ECHO-UNCERTAINTY-DISPATCH")

    # Input
    prompt: str = Field(..., description="Original user prompt")
    prompt_sha256: str = Field(..., description="SHA256 of prompt")

    # Detection
    uncertainty_type: str = Field(
        ...,
        description="Type: temporal, external, codebase, or general"
    )
    aggregate_confidence: float = Field(
        ...,
        ge=0.0, le=1.0,
        description="Max confidence from other detectors before uncertainty triggered"
    )
    matched_keywords: List[str] = Field(default_factory=list)
    matched_patterns: List[str] = Field(default_factory=list)

    # Dispatch
    agents_dispatched: List[str] = Field(
        ...,
        description="List of agent types that were dispatched"
    )
    agent_results: List[AgentResultModel] = Field(
        default_factory=list,
        description="Results from each dispatched agent"
    )

    # Synthesis
    answer: str = Field(..., description="Synthesized answer from agent results")
    answer_sha256: str = Field(..., description="SHA256 of answer")
    sources_cited: List[str] = Field(
        default_factory=list,
        description="Sources cited in final answer"
    )

    # Chain verification
    # chain_hash = SHA256(prompt_sha || uncertainty_type || agent_hashes || answer_sha)
    chain_hash: str = Field(
        ...,
        description="SHA256 chain hash for verification"
    )

    # Status
    status: Literal["PASS", "PARTIAL", "UNIMPLEMENTED"] = Field(
        default="PASS",
        description="PASS=all agents succeeded, PARTIAL=some failed, UNIMPLEMENTED=stub agents"
    )
    errors: List[str] = Field(default_factory=list)


class GeometryRoutingCDR(BaseModel):
    """
    CDR envelope for geometry routing decision.

    Logged for observability when geometry intent is detected.
    """
    cdr_version: str = Field(default="v1.1")
    what: Dict[str, str] = Field(
        default_factory=lambda: {"task": "geometry_routing", "intent": "spatial_query"}
    )
    why: Dict[str, Any] = Field(
        ...,
        description="Trigger, matched_keywords, confidence"
    )
    links: Dict[str, str] = Field(
        ...,
        description="Tool endpoint, graph node references"
    )


# ============================================================================
# /echo/ask - Memory Search & Answer Models (WO-ECHO-ASK-02)
# ============================================================================

class AskMode(str, Enum):
    """Mode for /echo/ask requests."""
    ANSWER = "answer"       # Full evidence + answer + confidence
    SEARCH = "search"       # Just search results (chunks)
    RECEIPT = "receipt"     # Find specific receipt by ID/path


class AskRequest(BaseModel):
    """Request for /echo/ask - Memory search with evidence."""
    query: str = Field(..., description="Search query or question")
    k: int = Field(default=8, ge=1, le=50, description="Number of results to return")
    mode: AskMode = Field(default=AskMode.ANSWER, description="Response mode")
    receipt_id: Optional[str] = Field(
        default=None,
        description="Receipt ID/path when mode=receipt"
    )
    include_chunks: bool = Field(
        default=False,
        description="Include longer evidence snippets (still truncated)"
    )


class AskEvidence(BaseModel):
    """Evidence item from memory search."""
    source: str = Field(..., description="Source path with line reference")
    sha256: Optional[str] = Field(default=None, description="SHA256 of source content")
    snippet: str = Field(..., description="Truncated content snippet")
    source_type: Optional[str] = Field(default=None, description="Type: code, receipt, doc")
    score: Optional[float] = Field(default=None, description="Relevance score 0-1")


class AskResponse(BaseModel):
    """Response for /echo/ask - Evidence-grounded answer."""
    status: Literal["OK", "ERROR", "NO_INDEX"] = Field(
        ...,
        description="OK=success, ERROR=failure, NO_INDEX=index not available"
    )
    mode: AskMode
    query: str
    answer: Optional[str] = Field(
        default=None,
        description="Synthesized answer (mode=answer only)"
    )
    confidence: Optional[str] = Field(
        default=None,
        description="HIGH, MEDIUM, or LOW (mode=answer only)"
    )
    evidence: List[AskEvidence] = Field(
        default_factory=list,
        description="Evidence items with sources and snippets"
    )
    receipt_data: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Receipt content (mode=receipt only)"
    )
    receipt_path: str = Field(
        ...,
        description="Path to this request's receipt"
    )
    chain_hash: str = Field(
        ...,
        description="SHA256 chain hash for verification"
    )
    error: Optional[str] = Field(default=None, description="Error message if status=ERROR")


class AskReceipt(BaseModel):
    """Receipt for /echo/ask execution."""
    receipt_type: Literal["echo_ask"] = "echo_ask"
    work_order: str = "WO-ECHO-ASK-02"
    timestamp_utc: str
    receipt_id: str

    query: str
    query_sha256: str
    mode: str
    k: int

    result_sha256: str = Field(..., description="SHA256 of answer/results")
    evidence_sha256: str = Field(..., description="SHA256 of serialized evidence")
    answer_preview: Optional[str] = Field(default=None, description="Truncated answer")
    evidence_preview: List[str] = Field(
        default_factory=list,
        description="Truncated evidence snippets"
    )

    status: Literal["OK", "ERROR", "NO_INDEX"]
    error: Optional[str] = None

    chain_hash: str = Field(
        ...,
        description="SHA256(query_sha || mode || k || result_sha || evidence_sha || status)"
    )
