# guardianhub_sdk/agents/specialist_base.py
import json
import uuid
from typing import Dict, Any, List, Optional, TypeVar, Union
from fastapi import APIRouter, HTTPException,BackgroundTasks

from guardianhub.clients.llm_client import LLMClient

from guardianhub.clients.consul_client import ConsulClient
from guardianhub.clients.tool_registry_client import ToolRegistryClient
from guardianhub.config.settings import settings
from guardianhub.agents.workflows.constants import get_all_activities
from guardianhub.agents.workflows.agent_contract import ActivityRoles
from guardianhub.models.agent.contracts.action import ActionStep,ActionOutcome
from guardianhub.models.agent.contracts import DiscoveryRequest,DiscoveryResponse
from guardianhub.models.agent.contracts import  HistoryRequest,HistoryResponse
from guardianhub.models.agent.contracts.tactical import TacticalBundle,TacticalAuditReport,TACTICAL_RISK_PROMPT
from guardianhub.models.agent.contracts.attainment import AttainmentCheck,AttainmentReport
from guardianhub.models.agent.contracts.debrief import SummaryBrief,IntelligenceReport
from guardianhub.models.agent.contracts.learning import AfterActionReport , LearningAnchor
from guardianhub.models.agent.contracts.completion import  MissionManifest, CallbackAck
from guardianhub.models.agent.contracts.support import  SupportRequest,PeerSupportOutcome
from guardianhub.models.agent_models import AgentSubMission
from guardianhub.models.base import BaseModel
from guardianhub.models.builtins.vector_models import VectorQueryRequest, VectorQueryResponse
from guardianhub.models.template.agent_plan import MacroPlan, MissionRequest
from guardianhub.agents.services.memory_manager import MemoryManager
from guardianhub.agents.services.episodic_manager import EpisodicManager
from guardianhub.clients.a2a_client import A2AClient
from guardianhub.clients.vector_client import VectorClient
from guardianhub.clients.graph_db_client import GraphDBClient
from guardianhub.clients.tool_registry_client import ToolRegistryClient
from guardianhub.clients.classification_client import ClassificationClient
from guardianhub.clients.ocr_client import OCRClient
from guardianhub.clients.paperless_client import PaperlessClient
from guardianhub.clients.text_cleaner_client import TextCleanerClient
from guardianhub import get_logger
from temporalio import activity
from httpx import AsyncClient

logger = get_logger(__name__)

# Type variable for client types
T = TypeVar('T')


class SovereignSpecialistBase:
    """
    The Foundation for all Specialist Agents.
    Encapsulates Planning, Execution, and Context Management.
    """

    def __init__(self, activities_instance: Any, **kwargs):
        # 1. Identity
        self.spec = settings.specialist
        self.name = settings.specialist.agent_name

        # 2. Standardize Core Clients (from kwargs or defaults)
        self.llm = kwargs.get('llm_client') or LLMClient()
        self.vector_client = kwargs.get('vector_client') or VectorClient()
        self.graph_client = kwargs.get('graph_client') or GraphDBClient()
        self.tool_registry_client = kwargs.get('tool_registry_client') or ToolRegistryClient()
        self.consul_client = kwargs.get('consul_client') or ConsulClient()
        self.temporal_client = kwargs.get('temporal_client')

        # 🎯 FIX: Initialize custom_clients so get_client() works
        self.custom_clients: Dict[str, Any] = kwargs.get('custom_clients', {})

        # 3. Initialize core services (Senses)
        self.episodes = EpisodicManager(
            vector_client=self.vector_client,
            graph_client=self.graph_client,
            tool_registry=self.tool_registry_client
        )
        # The Brain wraps the Librarian to ensure context assembly uses standardized Pillar logic
        self.memory = MemoryManager(episodic_manager=self.episodes)

        self.a2a = A2AClient(sender_name=self.name, consul_service=self.consul_client)

        # 4. Domain Activity Instance (The Muscle)
        self.activities_instance = activities_instance

        # 🧬 THE FUSION MOVE
        if hasattr(self.activities_instance, "fuse_with_base"):
            self.activities_instance.fuse_with_base(self)

        # 5. API Gateway Setup
        self.router = APIRouter(prefix="/v1/mission")
        self._setup_routes()

    def get_client(self, client_name: str, client_type: Optional[T] = None) -> Optional[T]:
        """
        Retrieves an extension client from the custom registry.
        """
        client = self.custom_clients.get(client_name)
        if client is not None and client_type is not None and not isinstance(client, client_type):
            raise TypeError(f"Client {client_name} is not of type {client_type.__name__}")
        return client


    def get_activities(self) -> list:
        from temporalio import activity
        registry = {}

        # 1. HARVEST EXPLICIT OVERRIDES (Muscle)
        if hasattr(self.activities_instance, "get_muscle_registry"):
            muscle_map = self.activities_instance.get_muscle_registry()
            for act_name, method in muscle_map.items():
                # Soul Extraction (for metadata pinning)
                target = method.__func__ if hasattr(method, "__func__") else method

                if not hasattr(target, "__temporal_activity_definition"):
                    activity.defn(target, name=act_name)

                # Signature Promotion
                defn = getattr(target, "__temporal_activity_definition")
                target._defn = defn

                # 🟢 RETURN THE BODY: Keep it bound to the specialist instance
                registry[defn.name] = method
                logger.info(f"✅ [FUSED] Muscle capability: {act_name}")

        # 2. HARVEST KERNEL FALLBACKS (SDK Base)
        import inspect
        for _, method in inspect.getmembers(self, predicate=inspect.iscoroutinefunction):
            target = method.__func__ if hasattr(method, "__func__") else method
            defn = getattr(target, "__temporal_activity_definition", None)

            if defn and defn.name not in registry:
                # Signature Promotion
                target._defn = defn
                # 🟢 RETURN THE BODY: Keep it bound to 'self' (the SDK base)
                registry[defn.name] = method
                logger.info(f"🛡️ [KERNEL] Fallback active: {defn.name}")

        return list(registry.values())

    def _setup_routes(self):
        @self.router.post("/propose", summary="Sovereign Planning Handshake")
        async def propose(mission: AgentSubMission):
            activities = self.get_activities()
            try:
                # 🎯 Finding the Architect
                plan_activity = next(a for a in activities if a._defn.name == ActivityRoles.PROPOSAL)

                # Execute domain planning logic
                proposal: MacroPlan = await plan_activity(mission)

                return {
                    "plan": proposal.steps,
                    "rationale": proposal.reflection,
                    "confidence_score": proposal.confidence_score,
                    "session_id": mission.session_id,
                    "metadata": proposal.metadata
                }
            except StopIteration:
                raise HTTPException(status_code=501, detail=f"Agent {self.name} has no PROPOSAL capability.")

        @self.router.post("/execute")
        async def execute_mission(payload: MissionRequest, background_tasks: BackgroundTasks):
            """
            Sutram Entry Point.
            Pydantic will automatically validate the JSON against MissionRequest.
            """
            logger.info(f"🛰️ Received authorized mission: {payload.session_id}")

            # 🧪 Pressure Test: Ensure steps actually exist
            if not payload.macro_plan:
                logger.warning(f"⚠️ Warning: Mission {payload.session_id} has 0 steps in plan.")

            try:
                # Trigger Temporal via Background Task or Direct Call
                # Ensure your Temporal Client is using the pydantic_data_converter!
                background_tasks.add_task(
                    self.temporal_client.start_workflow,
                    "SpecialistMissionWorkflow",
                    payload.model_dump(),  # Serializes cleanly now
                    id=f"mission-{payload.session_id}",
                    task_queue=settings.temporal.task_queue
                )

                return {"status": "ACCEPTED", "session_id": payload.session_id}

            except Exception as e:
                logger.error(f"❌ Failed to trigger workflow: {str(e)}")
                raise HTTPException(status_code=500, detail="Workflow Trigger Failed")

    # ============================================================================
    # SOVEREIGN SPECIALIST ACTIVITY METHODS - Organized by ActivityRoles Sequence
    # ============================================================================
    @activity.defn(name=ActivityRoles.RECON)
    async def conduct_reconnaissance(self, request_input: Any) -> DiscoveryResponse:
        """🎯 SIGNATURE FIX: Accepts Any to handle list/dict variations."""
        raw_req = request_input[0] if isinstance(request_input, list) else request_input
        request = DiscoveryRequest.model_validate(raw_req)
        logger.info(f"📡 [RECON] Environment: {request.environment} | Target: {request.sub_objective}")
        start_time = activity.datetime.now()

        try:
            # 1. Access the Multi-Tiered Memory Manager
            # Inside SpecialistBase.conduct_reconnaissance
            capabilities = await self.tool_registry_client.load_agentic_capabilities()
            refined_query = " ".join(request.approved_keywords) if request.approved_keywords else request.sub_objective

            # 🚀 THE MATSYA INJECTION:
            # We pass the boundaries (Domains & Signals) into the Brain
            context = await self.memory.get_reasoning_context(
                query=refined_query,
                template_id=request.template_id,
                capabilities=capabilities,
                # Pass these as a unified intent bundle
                intent_context={
                    "archetype": request.suggested_archetype,
                    "whitelist": request.target_domains,
                    "signals": request.expected_signals,
                    "environment": request.environment
                }
            )

            # 2. Build the Standardized Response
            # (We will use this logic to calculate 'signal_attainment' in the next step)
            found_facts = context.get("facts", [])

            # Simple string-matching for attainment (First-pass Truth)
            attained = [s for s in request.expected_signals if any(s.lower() in str(f).lower() for f in found_facts)]
            score = len(attained) / len(request.expected_signals) if request.expected_signals else 1.0

            # 2. Build the Standardized Response
            response = DiscoveryResponse(
                success=True,
                correlation_id=request.session_id,
                facts=found_facts,
                beliefs=context.get("beliefs", []),
                episodes=context.get("episodes", []),
                # 🐢 THE KURMA UPDATE:
                infrastructure_map={d: "PENDING_VERIFICATION" for d in request.target_domains},
                signal_attainment=score,
                metadata={
                    **context.get("metadata", {}),
                    "attained_signals": attained,
                    "archetype_hint": request.suggested_archetype
                },
                telemetry={
                    "duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000,
                    "fact_count": len(context.get("facts", [])),
                    "belief_count": len(context.get("beliefs", [])),
                    "episode_count": len(context.get("episodes", [])),
                    "attainment_score": score
                }
            )

            logger.info(
                f"✅ [RECON] {request.suggested_archetype} | Signals: {len(attained)}/{len(request.expected_signals)}")
            logger.info(f"✅ [RECON] Discovery complete. Total Intelligence Items: {response.total_count}")
            return response

        except Exception as e:
            logger.error(f"📡 [RECON] Discovery failed: {str(e)}")
            return DiscoveryResponse(
                success=False,
                correlation_id=request.session_id,
                error_message=str(e),
                telemetry={"duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000}
            )

    @activity.defn(name=ActivityRoles.HISTORY)
    async def retrieve_intelligence_history(self, request: HistoryRequest) -> HistoryResponse:
        """
        SDK Default: Episodic Memory Retrieval.
        🎯 SIGNATURE: No more unfilled parameters.
        """
        logger.info(f"📚 [HISTORY] Querying past Leela for: {request.search_query}")
        start_time = activity.datetime.now()

        try:
            # 1. Fetch episodes (Assuming the manager returns objects with a 'similarity' field)
            hindsight = await self.episodes.get_hindsight(
                current_task=request.search_query,
                template_id=request.template_id,
            )


            # 2. CALCULATION: Fill 'avg_similarity_score'
            # Extract scores if they exist, otherwise default to 0.0
            scores = [float(h.get("similarity", 0.0)) for h in hindsight]
            avg_score = sum(scores) / len(scores) if scores else 0.0

            # 3. EXTRACTION: Pull Lessons
            lessons = [h.get("aha_moment") for h in hindsight if h.get("aha_moment")]

            response = HistoryResponse(
                success=True,
                correlation_id=request.session_id,
                episodes=hindsight,
                lessons_discovered=lessons,
                avg_similarity_score=round(avg_score, 4),  # 🎯 Filled
                error_message="None",  # 🎯 Explicitly unfilled
                telemetry={
                    "duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000,
                    "episode_count": len(hindsight)
                }
            )

            logger.info(f"✅ [HISTORY] Found {len(hindsight)} parallels. Avg Similarity: {avg_score:.2f}")
            return response

        except Exception as e:
            logger.error(f"📚 [HISTORY] Memory retrieval failed: {str(e)}")
            # 🎯 FILLING ERROR STATE
            return HistoryResponse(
                success=False,
                correlation_id=request.session_id,
                avg_similarity_score=0.0,
                error_message=f"Episodic Retrieval Error: {str(e)}",  # 🎯 Filled
                telemetry={"duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000}
            )

    @activity.defn(name=ActivityRoles.TACTICAL)
    async def analyze_tactical_context(self, bundle_input: Any) -> TacticalAuditReport:
        """
        SDK Default: Real-time Risk Assessment.
        🎯 SIGNATURE: Replaces 'Any' with 'TacticalBundle'.
        """
        raw = bundle_input[0] if isinstance(bundle_input, list) else bundle_input
        bundle = TacticalBundle.model_validate(raw)

        logger.info(f"🏛️ [TACTICAL] Auditing {len(bundle.proposed_plan.steps)} steps for {bundle.agent_name}")

        # 🟢 TEMPORAL BEST PRACTICE: Using activity.now() or datetime through Temporal
        start_time = activity.datetime.now()

        # 1. Hydrate the Audit Context
        audit_context = {
            "steps": bundle.proposed_plan.model_dump(),
            "facts": bundle.recon_intelligence.facts,
            "beliefs": bundle.recon_intelligence.beliefs,
            "past_lessons": bundle.history_intelligence.lessons_discovered
        }

        try:

            # 1. Manually stringify your context data (Plan + Recon)
            # We do this so the LLM still has 'Eyes', but the SDK sees no 'context' object
            manual_data_block = f"""
            PROPOSED PLAN: {bundle.proposed_plan.steps}
            RECONNAISSANCE: {bundle.recon_intelligence.facts if hasattr(bundle.recon_intelligence, 'facts') else 'None'}
            """

            # 2. Invoke the Safety Officer (LLM) WITHOUT the context parameter
            # This mirrors the MacroPlan call structure exactly
            analysis = await self.llm.invoke_structured_model(
                user_input=f"Safety audit for mission: {bundle.proposed_plan.reflection}\n\nDATA:\n{manual_data_block}",
                system_prompt_template=TACTICAL_RISK_PROMPT,
                # 🎯 REMOVED: context=audit_context,
                response_model_name="RiskAnalysisReport",
                model_key="judge"
            )
            # 🚀 THE FIX: Use getattr with proper fallbacks for Pydantic objects
            # This works whether 'analysis' is a Pydantic object or a raw dict
            risk_score = getattr(analysis, "risk_score", 1.0)
            decision = getattr(analysis, "decision", "HALT")
            reason = getattr(analysis, "reason", "Audit synthesis complete")
            mods = getattr(analysis, "mitigation_suggested", [])

            # Business Logic: Proceed only if risk is low and decision isn't HALT
            final_decision = "PROCEED" if risk_score < bundle.risk_threshold and decision != "HALT" else "HALT"


            response = TacticalAuditReport(
                success=True,
                correlation_id=bundle.session_id,
                decision=final_decision,
                risk_score=risk_score,
                justification=reason,
                suggested_modifications=mods,
                error_message="None",  # 🎯 FIXED: Explicitly clear error state
                telemetry={
                    "duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000,
                    "audited_steps": len(bundle.proposed_plan.steps)
                }
            )

            logger.info(f"🏛️ [TACTICAL] Decision: {decision} (Risk: {risk_score})")
            return response

        except Exception as e:
            logger.error(f"🏛️ [TACTICAL] Audit failure: {str(e)}")
            # 🎯 FIXED: Define risk_score locally for exception case
            risk_score = 1.0  # Default to high risk on failure
            return TacticalAuditReport(
                success=False,
                correlation_id=bundle.session_id,
                decision="HALT",
                risk_score=risk_score,
                error_message=f"LLM_AUDIT_FAILURE: {str(e)}",
                justification="Audit system failure. Defensive HALT triggered to protect infrastructure.",
                telemetry={"duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000}
            )



    @activity.defn(name=ActivityRoles.PROPOSAL)
    async def formulate_mission_proposal(self, mission: AgentSubMission) -> MacroPlan:
        """
        SDK Default: Generates the plan.
        Note: This is usually called via /propose, but can be a durable activity.
        """
        logger.info(f"📋 [PROPOSAL] Formulating mission proposal for: {mission.sub_objective}")
        logger.info(f"📋 [PROPOSAL] Agent: {self.name} creating structured execution plan")
        
        try:
            # RECOMMENDED FIX
            plan = await self.llm.invoke_structured_model(
                system_prompt_template="You are the Architect. Generate a plan for: {goal}",
                context={"goal": mission.sub_objective},  # 🎯 Hydrate properly
                user_input=mission.sub_objective,
                response_model_name="MacroPlan",
                model_key="judge"
            )
            logger.info(f"📋 [PROPOSAL] Generated plan with {len(plan.steps)} steps")
            logger.info(f"📋 [PROPOSAL] Plan confidence: {plan.confidence_score:.2f}")
            return plan
        except Exception as e:
            logger.error(f"📋 [PROPOSAL] Failed to generate mission proposal: {str(e)}")
            raise

    @activity.defn(name=ActivityRoles.INTERVENTION)
    async def execute_direct_intervention(self, step_input: Any) -> ActionOutcome:
        """
        🎯 DHARMA FIX: Universal Step Receiver.
        """
        logger.info(f"⚡ [INTERVENTION] Raw Input Type: {type(step_input)}")
        start_time = activity.datetime.now()

        # 🛰️ PRE-FLIGHT: Capture session_id safely for the error fallback
        # We do this before the 'try' because we need it for the 'except'
        raw_data = step_input[0] if isinstance(step_input, list) and step_input else step_input
        session_id = raw_data.get("session_id", "unknown_session") if isinstance(raw_data, dict) else getattr(raw_data,
                                                                                                              "session_id",
                                                                                                              "unknown_session")

        try:
            # 1. VALIDATION: Use the unified model
            if not isinstance(raw_data, ActionStep):
                step = ActionStep.model_validate(raw_data)
            else:
                step = raw_data

            # 2. EXECUTION: Pass to domain logic
            muscle_result = await self.activities_instance.execute_direct_intervention(step)

            # Ensure muscle_result is a dict or object
            m_status = muscle_result.get("status", "SUCCESS") if isinstance(muscle_result, dict) else getattr(
                muscle_result, "status", "SUCCESS")
            m_obs = muscle_result.get("observation", "Executed.") if isinstance(muscle_result, dict) else getattr(
                muscle_result, "observation", "Executed.")

            # 3. WRAP
            return ActionOutcome(
                success=True,
                correlation_id=step.session_id,
                status=m_status,
                observation=m_obs,
                raw_result=muscle_result.get("data", {}) if isinstance(muscle_result, dict) else getattr(muscle_result,
                                                                                                         "data", {}),
                telemetry={
                    "duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000,
                    "tool_used": step.tool_name  # 🚀 COORDINATION: Use tool_name from PlanStep
                }
            )

        except Exception as e:
            logger.error(f"❌ [INTERVENTION] Handshake crash: {str(e)}")
            # 🛡️ THE FIX: Must provide ALL required fields to avoid Pydantic ValidationError
            return ActionOutcome(
                success=False,
                correlation_id=session_id,  # 🎯 Safe capture from step_input
                status="FAILED",  # 🎯 Required field
                observation="None",  # 🎯 Required field
                error_message=f"Forensic error: {str(e)}"
            )

    @activity.defn(name=ActivityRoles.ATTAINMENT)
    async def verify_objective_attainment(self, check: AttainmentCheck) -> AttainmentReport:
        """
        SDK Default: Objective Attainment Inspector.
        🎯 SIGNATURE: Uses the ActionOutcome metrics/analysis to verify truth.
        """
        logger.info(f"🧐 [ATTAINMENT] Inspecting {check.step_id} (Mode: {check.verification_mode})")
        start_time = activity.datetime.now()

        try:
            # 1. Check for Muscle Implementation
            if hasattr(self.activities_instance, "verify_objective_attainment"):
                attained = await self.activities_instance.verify_objective_attainment(check)
            else:
                # 2. SDK Fallback: Use the metrics/analysis we just built!
                # If anomaly_detected is True, we fail attainment by default.
                is_anomaly = check.action_outcome.analysis.get("anomaly_detected", False)
                attained = check.action_outcome.success and not is_anomaly

            return AttainmentReport(
                success=True,
                correlation_id=check.session_id,
                attained=attained,
                certainty_score=0.9 if attained else 0.1,
                error_message="None",
                telemetry={"duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000}
            )

        except Exception as e:
            logger.error(f"❌ [ATTAINMENT] Inspection failure: {str(e)}")
            return AttainmentReport(
                success=False,
                correlation_id=check.session_id,
                attained=False,
                discrepancy_found=f"EXCEPTION: {str(e)}",  # 🎯 Fixed from bool to str
                error_message=str(e)
            )

    @activity.defn(name=ActivityRoles.DEBRIEF)
    async def summarize_intelligence_debrief(self, brief_input: Any) -> IntelligenceReport:
        """🎯 DHARMA FIX: Resilient Debrief (Pydantic Uniformity)."""
        start_time = activity.datetime.now()

        # 1. Normalize Input
        raw = brief_input[0] if isinstance(brief_input, list) else brief_input

        try:
            # 2. Hydrate Model (brief is now an OBJECT)
            brief = SummaryBrief.model_validate(raw)
            logger.info(f"📝 [DEBRIEF] Synthesizing report for: {brief.original_objective}")

            total_metrics = {}
            outcomes_str_list = []

            for report in brief.step_results:
                # 🚀 OBJECT ACCESS: report is likely AttainmentReport
                # Aggregate metrics safely
                metrics = getattr(report, "telemetry", {})
                for k, v in metrics.items():
                    if isinstance(v, (int, float)):
                        total_metrics[k] = total_metrics.get(k, 0.0) + v

                # Build narrative strings using object attributes
                status_label = "✅ SUCCESS" if getattr(report, "attained", False) else "❌ FAILED"

                # Use getattr for maximum resilience if fields might be missing
                step_id = getattr(report, "step_id", "unknown")
                score = getattr(report, "certainty_score", 0.0)

                outcomes_str_list.append(
                    f"- Step ID: {step_id} | Status: {status_label} | Certainty: {score}"
                )

            # 3. LLM Input Construction
            outcomes_str = "\n".join(outcomes_str_list)
            manual_user_input = f"""
                MISSION OBJECTIVE: {brief.original_objective}

                MISSION OUTCOMES:
                {outcomes_str}

                Analyze the above and provide a structured debrief.
                """

            # 4. Invoke LLM
            report_output = await self.llm.invoke_structured_model(
                user_input=manual_user_input,
                system_prompt_template=self._get_debrief_prompt_template(),
                response_model_name="MissionDebriefReport",
                model_key="judge"
            )

            return IntelligenceReport(
                success=True,
                correlation_id=brief.session_id,
                narrative_summary=report_output.summary,
                key_takeaways=report_output.key_takeaways,
                final_status="PARTIAL" if brief.is_partial_success else "SUCCESS",
                impact_metrics=total_metrics,
                error_message="None",
                telemetry={"duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000}
            )

        except Exception as e:
            logger.error(f"❌ [DEBRIEF] Synthesis failed: {str(e)}")
            # Safeguard: try to get session_id from raw if brief failed to hydrate
            s_id = getattr(brief, 'session_id', raw.get('session_id', 'unknown')) if 'brief' in locals() else raw.get(
                'session_id', 'unknown')
            return IntelligenceReport(
                success=False,
                correlation_id=s_id,
                narrative_summary="Technical failure during report generation.",
                error_message=str(e)
            )

    @activity.defn(name=ActivityRoles.AAR)
    async def commit_mission_after_action_report(self, report_input: Any) -> LearningAnchor:
        """🎯 DHARMA FIX: Resilient Wisdom Anchoring."""
        logger.info(f"🗃️ [AAR] Input Type: {type(report_input)}")

        try:
            # 1. NORMALIZATION
            # If the input is a list (raw results), we wrap it. If it's a dict (synthesized report), we use it.
            raw_report = report_input[0] if isinstance(report_input, list) and len(report_input) > 0 else report_input

            # 2. VALIDATION
            report = AfterActionReport.model_validate(raw_report)
            logger.info(f"📝 Anchoring Wisdom for Mission: {report.mission_id}")

            # 3. ANCHORING: Commit to VectorDB (The Leela Pillar)
            doc_id = f"aar-{report.mission_id}"
            await self.vector_client.upsert_atomic(
                collection="episodes",
                doc_id=doc_id,
                text=f"{report.summary_brief}\n\nKey Insight: {report.aha_moment}",
                metadata={
                    "agent": report.agent_name,
                    "session_id": report.session_id,
                    "success_score": report.success_score,
                    "template_id": report.template_id,
                    "ingested_at": activity.datetime.now().isoformat()
                }
            )

            # 🚀 THE CRITICAL RETURN: No more Booleans!
            return LearningAnchor(
                success=True,
                memory_id=doc_id,
                indexed_at=activity.datetime.now(),
                correlation_id=report.session_id,
                produced_at=activity.datetime.now()
            )

        except Exception as e:
            logger.error(f"❌ [AAR] Anchoring Failed: {str(e)}")
            # Even in failure, return the model so the workflow doesn't crash on .get()
            return LearningAnchor(success=False, memory_id="none", error_message=str(e))

    @activity.defn(name=ActivityRoles.COMPLETION)
    async def transmit_mission_completion(self, manifest_input: Any) -> CallbackAck:
        """🎯 DHARMA FIX: Bulletproof Manifest Handshake."""

        # Initialize session_id for the error fallback
        session_id = "unknown"

        try:
            # 1. NORMALIZATION
            raw_manifest = manifest_input[0] if isinstance(manifest_input, list) else manifest_input
            manifest = MissionManifest.model_validate(raw_manifest)
            session_id = manifest.session_id  # Capture for fallback

            # 2. SANITIZATION: Force the report to be a clean JSON-safe dict
            # This prevents the 'Object of type type' error
            sanitized_report = json.loads(json.dumps(manifest.final_report, default=str))

            # 3. PAYLOAD CONSTRUCTION
            payload = {
                "agent": self.name,
                "mission_id": manifest.mission_id,
                "session_id": manifest.session_id,
                "report": sanitized_report,  # 🟢 Cleaned
                "status": manifest.mission_status,
                "trace_id": manifest.trace_id,
                "completed_at": activity.datetime.now().isoformat()  # 🟢 Instance call
            }

            async with AsyncClient() as client:
                logger.info(f"📡 [COMPLETION] Calling Sutram at: {manifest.callback_url}")
                response = await client.post(manifest.callback_url, json=payload, timeout=30.0)

                success = response.is_success
                return CallbackAck(
                    success=success,
                    correlation_id=session_id,  # 🟢 Required field
                    orchestrator_received=success,
                    error_message="None" if success else f"HTTP_{response.status_code}"
                )

        except Exception as e:
            logger.error(f"❌ [COMPLETION] Failure: {str(e)}")
            # 🎯 THE RESILIENT FALLBACK
            return CallbackAck(
                success=False,
                correlation_id=session_id,  # 🟢 satisfy Pydantic
                orchestrator_received=False,
                error_message=str(e)
            )


    @activity.defn(name=ActivityRoles.SUPPORT)
    async def recruit_specialist_support(self, request: SupportRequest) -> PeerSupportOutcome:
        """
        SDK Default: Peer Collaboration Handshake.
        🎯 SIGNATURE: Replaces Dict with 'SupportRequest' and 'PeerSupportOutcome'.
        """
        logger.info(f"🤝 [SUPPORT] Recruiting {request.target_specialty} for: {request.objective[:50]}...")
        start_time = activity.datetime.now()

        try:
            # 1. Access the A2A (Agent-to-Agent) Client
            # The A2A client handles the Consul discovery and NATS/HTTP routing
            result = await self.a2a.recruit_ranger(
                specialty=request.target_specialty,
                objective=request.objective,
                priority=request.priority,
                context=request.shared_context
            )

            return PeerSupportOutcome(
                success=True,
                correlation_id=request.session_id,
                peer_name=result.get("agent_name", "unknown_ranger"),
                mission_id=result.get("mission_id", "pending"),
                recruitment_status=result.get("status", "ACCEPTED"),
                peer_metadata=result.get("metadata", {}),
                error_message="None",
                telemetry={"duration_ms": (activity.datetime.now() - start_time).total_seconds() * 1000}
            )

        except Exception as e:
            logger.error(f"❌ [SUPPORT] Recruitment failed: {str(e)}")
            return PeerSupportOutcome(
                success=False,
                correlation_id=request.session_id,
                peer_name="None",
                mission_id="None",
                recruitment_status="FAILED",
                error_message=str(e)
            )


    async def _run_result_analytics(self, raw_output: Dict[str, Any]) -> Dict[str, float]:
        """
        Placeholder: Extract quantitative metrics from tool output.
        Hook for: Time-series ingestion, latency tracking, cost calculation.
        """
        # 🎯 Dummy Logic: In the future, this calls your TimeSeries service
        metrics = {
            "processing_time_ms": raw_output.get("latency", 0.0),
            "payload_size_kb": float(len(str(raw_output)) / 1024),
            "confidence_index": raw_output.get("confidence", 1.0)
        }

        # If the tool returned specific numeric data (e.g., price), promote it to metrics
        if "price" in raw_output:
            metrics["extracted_value"] = float(raw_output["price"])

        return metrics

    async def _check_anomaly_signatures(self, raw_output: Dict[str, Any]) -> Dict[str, Any]:
        """
        Placeholder: Qualitative risk/anomaly analysis.
        Hook for: Outlier detection, pattern matching, safety violations.
        """
        # 🎯 Dummy Logic: Hook for your Risk Analytics engine
        analysis = {
            "anomaly_detected": False,
            "risk_tier": "LOW",
            "pattern_match": "STANDARD_EXECUTION"
        }

        # Simple heumission_idristic: If the tool output is empty, it's an anomaly
        if not raw_output or (isinstance(raw_output, dict) and not raw_output.get("data")):
            analysis["anomaly_detected"] = True
            analysis["risk_tier"] = "MEDIUM"
            analysis["pattern_match"] = "NULL_RESPONSE_PATTERN"

        return analysis

    # guardianhub_sdk/agents/specialist_base.py

    def _get_debrief_prompt_template(self) -> str:
        """
        The personas and structure for the Rama (Debrief) phase.
        Ensures the LLM acts as a synthesizer, not just a logger.
        """
        return """
        Role: Sovereign Mission Scribe (Rama Persona)
        Objective: Synthesize the execution results of a mission into a strategic debrief.

        MISSION CONTEXT:
        - Goal: {objective}
        - Steps Taken: {outcomes}

        INSTRUCTIONS:
        1. Summarize the narrative: What was the 'Hero's Journey' of this mission?
        2. Identify Friction: Where did the reality differ from the plan?
        3. Highlight the 'Aha!': What did we learn that we didn't know during RECON?
        4. Verdict: Is the goal attained?

        OUTPUT FORMAT:
        Narrative summary first, followed by a 'TAKEAWAYS:' section with bullet points.
        """

    # guardianhub_sdk/agents/specialist_base.py

    def _infer_aha_moment(self, narrative: str, data: List[Dict]) -> str:
        """
        Heuristic/LLM logic to distill a single 'Rule' from the mission.
        """
        # 🎯 Dummy Logic: In production, this would be a quick 1-shot LLM call
        # to summarize the 'Lesson' into a single sentence for Graph/Vector indexing.
        if "timeout" in narrative.lower():
            return "Observed network latency; recommend increasing retry backoff for this environment."

        failed_steps = [d for d in data if d.get("status") == "FAILED"]
        if failed_steps:
            return f"Mission friction encountered at step {failed_steps[0].get('step_id')}. Check tool dependencies."

        return "Standard execution successful. No anomalous patterns observed."