"""Agent Maestro NASA - Base class for FEID-MAS agents."""

# Centralized imports
from .imports import *

# CONFIGURACIÓN DE LOGGING PROFESIONAL
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s | [%(levelname)s] | %(message)s',
    handlers=[logging.StreamHandler(sys.stdout)]
)

# ============================================================================
# AGENT BASE CLASS
# ============================================================================

class AgenteMaestroNASA(ABC):
    def __init__(
        self,
        name: str,
        config: Optional[ConfigAgente] = None,
        handler: Optional[Callable] = None,
        auto_persist: bool = True,
        retry: Optional[RetryStrategy] = None,
    ):
        """Inicializa el agente base con configuración flexible.

        Args:
            name: Identificador único del agente.
            config: Objeto ConfigAgente con todos los parámetros (opcional).
            handler: Callback para persistencia externa.
            auto_persist: Habilitar persistencia local en JSONL.
            retry: Estrategia personalizada de reintentos.
        """
        # --- RESOLUCIÓN DE CONFIGURACIÓN ---
        if config is None:
            config = ConfigAgente()
        
        # Validar configuración antes de usarla
        try:
            config.validar()
        except ValueError as e:
            logging.error(f"Configuración inválida: {e}")
            raise
        
        # Guardar referencia a config
        self.config = config
        
        # Apply values from config
        self.capacity = config.capacidad
        self.max_retries = config.max_reintentos
        self.backpressure_threshold = config.backpressure_threshold
        self.default_ttl = config.ttl_defecto
        
        # --- BASE ATTRIBUTES ---
        self.name = name
        self.inbox = PriorityQueue()
        self.alive = True
        self.accepting_tasks = True  # Ingress control for graceful shutdown
        self.shutting_down = False   # Observable shutdown state
        self.state = "IDLE"
        self._last_activity = time.time()
        
        # Thread references (will be populated by AgentRuntime)
        self.worker_thread = None
        self.perception_thread = None

        # --- EXTENSION CONFIGURATION ---
        self.external_handler = handler
        self.auto_persist = auto_persist
        self.lock = threading.Lock()
        self.executor = (
            ThreadPoolExecutor(max_workers=config.max_workers) if config.max_workers else None
        )

        # --- ENTERPRISE FEATURES ---
        # Correlation IDs for distributed tracing (always enabled - core feature)
        self.correlation_tracker = CorrelationTracker()
        
        # Circuit breaker for fault tolerance (conditional)
        self.circuit_breaker = (
            CircuitBreaker(failure_threshold=config.circuit_breaker_threshold)
            if config.enable_circuit_breaker
            else None
        )
        
        # Customizable retry strategy (always enabled - core feature)
        self.retry_strategy = retry or ExponentialBackoffStrategy()
        
        # Lifecycle hooks (always enabled - core feature)
        self.lifecycle_hooks = LifecycleHooks()

        # Middleware stack for task processing (optional extensions)
        self.middleware_stack = MiddlewareStack()
        
        # Payload validator (always enabled - core feature)
        self.payload_validator = ValidadorPayload(max_bytes=config.payload_max_bytes)

        # Payload/result size limits and overflow policy
        self.max_task_bytes = config.max_task_bytes
        self.max_result_bytes = config.max_result_bytes
        self.payload_overflow_policy = config.payload_overflow_policy
        self.task_timeout_seconds = config.task_timeout_seconds
        self.handler_timeout_seconds = config.handler_timeout_seconds
        self.renew_ttl_on_retry = config.renew_ttl_on_retry
        self.agent_profile = config.agent_profile
        
        # Rate limiter (conditional)
        self.rate_limiter = (
            RateLimiter(
                tasa_tokens=config.rate_limit_tokens,
                capacidad_max=config.rate_limit_capacidad
            )
            if config.enable_rate_limiter
            else None
        )
        
        # Performance metrics collector (always enabled - core feature)
        self.metrics_collector = ColectorMetricas(ventana_segundos=config.metricas_ventana_segundos)
        
        # Queue policy manager (always enabled - core feature)
        self.queue_policy_manager = GestorColaPolitica(politica=config.politica_cola)
        
        # Structured auditor (conditional)
        self.auditor = (
            AuditorEstructurado(nombre_agente=name)
            if config.enable_auditor
            else None
        )
        
        # Error policy manager (always enabled - core feature)
        self.error_policy_manager = GestorPoliticasError(politica_defecto=config.politica_error_defecto)
        
        # Anti-starvation manager (conditional)
        self.anti_starvation_manager = (
            GestorAntiInanicion(
                ratio_minimo_baja_prioridad=config.anti_inanicion_ratio_minimo
            )
            if config.enable_anti_starvation
            else None
        )
        
        # Security sandbox with risky command denylist (conditional)
        self.security_sandbox = (
            SandboxSeguridad(
                habilitar_whitelist=config.sandbox_habilitar_whitelist
            )
            if config.enable_sandbox
            else None
        )
        
        # Health probe (Kubernetes-style health probes) (conditional)
        self.health_probe = (
            SondeoSalud(
                nombre_agente=name,
                umbral_inactividad=config.sondeo_umbral_inactividad
            )
            if config.enable_health_probes
            else None
        )

        # --- MEMORY PROTECTION (RAM) ---
        # Using deque to prevent infinite list growth if agent runs for months
        self.max_memory = config.max_memoria
        self.outbox = deque(maxlen=self.max_memory)
        self.incident_log = deque(maxlen=self.max_memory)
        self.dead_letter_queue = deque(maxlen=self.max_memory)

        # --- AUTOMATIC PERSISTENCE (ROTATION) ---
        self.max_records_per_file = config.max_registros_por_archivo
        self.record_counter = 0
        self.log_version = 1
        self.current_log_file = f"auto_log_{self.name}_v{self.log_version}.json"

        # --- COMPONENTS ---
        self.protocol_adapter = ProtocolAdapter(
            self.send_task,
            strict_mode=config.protocol_strict_mode,
        )
        self.event_sink = EventSink(self)
        self.queue_manager = QueueManager(
            self.inbox,
            capacity_getter=lambda: self.capacity,
            backpressure_threshold=self.backpressure_threshold
        )
        self.task_processor = TaskProcessor(
            self.circuit_breaker,
            self.retry_strategy,
            self.metrics_collector,
            self.auditor,
            executor=self.executor,
            max_retries=self.max_retries
        )
        self.agent_runtime = AgentRuntime(
            self.name,
            self._operational_cycle,
            self._perception_engine,
            self.lifecycle_hooks,
            self.executor
        )
        
        # --- FACADES (Simplified Interfaces) ---
        self.metrics = MetricsFacade(self.metrics_collector)
        self.security = SecurityFacade(self.security_sandbox) if self.security_sandbox else None
        self.health = HealthFacade(self.health_probe) if self.health_probe else None

        # --- METRICS ---
        self.global_metrics = {
            "successes": 0,
            "recovered_failures": 0,
            "fatal_failures": 0,
            "discarded": 0,
            "rejected": 0,
            "ttl_deaths": 0,
        }

        # Perception block
        self.perception_frequency = config.frecuencia_percepcion
        self.auto_start = config.auto_start
        
        # --- RUNTIME START ---
        if self.auto_start:
            self.agent_runtime.start()
        
        # --- INVOKE on_start HOOK ---
        # Note: on_start hook is already called by agent_runtime.start()


    # --- PERSISTENCE AND EVENTS SYSTEM ---

    def _register_to_auto_file(self, event_type, data):
        """Writes to disk record by record if enabled."""
        self.event_sink.register_to_auto_file(event_type, data)

    def _dispatch_event(self, event_type, data):
        """Sends event to RAM, Disk and External Handler."""
        self.event_sink.dispatch_event(event_type, data)

    # --- MULTI-PROTOCOL COMMUNICATION LAYER ---

    def receive_message(
        self,
        message,
        priority=2,
        complexity=5,
        ttl=10,
        return_ack: bool = False,
        backpressure: bool = False,
        max_backpressure_delay: float = 0.5,
    ):
        """Decode multi-protocol messages and optionally return an ack."""
        return self.protocol_adapter.receive_message(
            message,
            priority,
            complexity,
            ttl,
            return_ack=return_ack,
            backpressure=backpressure,
            max_backpressure_delay=max_backpressure_delay,
        )

    def send_task(
        self,
        task,
        priority=2,
        complexity=5,
        ttl=10,
        m_id=None,
        protocol="SIMPLE",
        backpressure=False,
        max_backpressure_delay=0.5,
        correlation_id: Optional[str] = None,
        return_metadata: bool = False,
    ):
        """Validate and enqueue a task with policy-aware queueing.

        Args:
            correlation_id: Correlation ID for distributed tracing (optional).
        
        Raises:
            ValueError: if parameters are invalid.
        """
        # Check if agent is accepting new tasks
        if not self.accepting_tasks:
            raise RuntimeError(
                f"Agent {self.name} is shutting down. No new tasks accepted."
            )
        
        if task is None:
            raise ValueError("Task cannot be None")
        if not isinstance(priority, int) or not 0 <= priority <= 5:
            raise ValueError("Priority must be an integer between 0 and 5")
        if not isinstance(complexity, int) or not 1 <= complexity <= 10:
            raise ValueError("Complexity must be an integer between 1 and 10")
        if not isinstance(ttl, (int, float)) or ttl <= 0:
            raise ValueError("TTL must be a number greater than 0")
        
        # Validate payload
        try:
            self.payload_validator.validar(task)
        except (TypeError, ValueError) as e:
            logging.error(f"Payload validation failed: {e}")
            with self.lock:
                self.global_metrics["rejected"] += 1
            return {"accepted": False, "reason": "PAYLOAD_INVALID"} if return_metadata else None

        # Security sandbox validation (denylist/whitelist)
        if self.security_sandbox and not self.security_sandbox.es_segura_tarea(task):
            self._register_incident("SECURITY_SANDBOX", task, "Sandbox policy rejected", m_id)
            with self.lock:
                self.global_metrics["rejected"] += 1
            return {"accepted": False, "reason": "SECURITY_SANDBOX"} if return_metadata else None
        
        # Rate limiting check
        origin = m_id or "default"
        if self.rate_limiter and not self.rate_limiter.permitir_por_origen(origin, costo=1.0):
            logging.warning(f"Rate limit exceeded for origin: {origin}")
            self._register_incident("RATE_LIMIT", task, "Limited by rate", m_id)
            with self.lock:
                self.global_metrics["rejected"] += 1
            return {"accepted": False, "reason": "RATE_LIMIT"} if return_metadata else None
        
        # Check queue capacity
        if not self.queue_manager.validate_capacity():
            self._register_incident("SATURATION", task, "Queue full", m_id)
            return {"accepted": False, "reason": "SATURATION", "retry_after": 0.0} if return_metadata else None
        
        # Apply backpressure if requested
        current_load = self.queue_manager.get_queue_load()
        retry_after = 0.0
        if backpressure:
            delay = self.queue_manager.apply_backpressure(max_backpressure_delay)
            if delay > 0:
                time.sleep(delay)
            retry_after = delay
        
        # Basic heuristic security evaluation
        task_text = str(task).lower()
        if (complexity > 7 and current_load > 0.8) or any(
            p in task_text for p in ["shutdown", "format", "delete_all"]
        ):
            self._register_incident("SECURITY", task, "Risk detected", m_id)
            return {"accepted": False, "reason": "SECURITY_RISK"} if return_metadata else None

        final_id = m_id if m_id else str(uuid.uuid4())[:8]
        
        # Create or track correlation ID for distributed tracing
        if correlation_id:
            self.correlation_tracker.create_correlation(correlation_id)
        else:
            correlation_id = self.correlation_tracker.create_correlation()
        
        # Notify plugin hooks (task received)
        self.lifecycle_hooks.invocar(
            "on_task_received",
            {
                "agent": self.name,
                "task_id": final_id,
                "task": task,
                "priority": priority,
                "complexity": complexity,
                "ttl": ttl,
                "protocol": protocol,
                "correlation_id": correlation_id,
                "timestamp": time.time(),
            },
        )

        # Register in audit
        if self.auditor:
            self.auditor.registrar_tarea_enviada(final_id, priority, ttl)
        
        # Apply queue policy (FIFO/LIFO/PRIORITY)
        policy = self.queue_policy_manager.politica
        arrival_time = time.time()
        effective_priority, arrival_override = self.queue_policy_manager.aplicar_politica(
            priority, arrival_time
        )

        # Enqueue task using queue manager
        self.queue_manager.enqueue_task(
            effective_priority,
            ttl,
            complexity,
            final_id,
            task,
            protocol,
            correlation_id,
            t_arrival_override=arrival_override,
        )

        if return_metadata:
            return {
                "accepted": True,
                "task_id": final_id,
                "retry_after": retry_after,
                "queue_policy": policy.value,
            }

        return final_id

    def cancel_task(self, m_id):
        """Cancels a queued task by its ID.

        Returns:
            True if removed, False if didn't exist.
        """
        return self.queue_manager.cancel_task(m_id)

    # --- PROCESSING ENGINE ---

    def _operational_cycle(self):
        """Main task processing loop with circuit breaker and retry strategy."""
        logging.info(f"AGENT {self.name}: NASA Industrial Mode Started.")
        while self.alive:
            try:
                # --- ANTI-STARVATION CHECK ---
                # If anti-starvation manager is enabled, check if we MUST process a low-priority task
                task_to_process = None
                manual_extraction = False
                if self.anti_starvation_manager and not self.anti_starvation_manager.puede_saltarse_baja_prioridad():
                    # Must process low-priority task. Search inbox for one.
                    task_to_process = self._find_low_priority_task()
                    if task_to_process is not None:
                        manual_extraction = True
                
                # If no low-priority task found (or not required), get normally from priority queue
                if task_to_process is None:
                    # Use configurable timeout from config for throughput optimization
                    # Default 0.1s (10x faster than 1s) balances throughput vs responsiveness
                    task_to_process = self.inbox.get(timeout=self.config.ciclo_timeout_segundos)
                
                # Unpack task with correlation_id
                prio, t_arrival, ttl, comp, attempts, m_id, task, proto, corr_id = task_to_process
                
                self.last_activity = time.time()

                # Record anti-starvation if enabled
                if self.anti_starvation_manager:
                    is_low_prio = prio >= 3  # Priority 3-5 considered "low"
                    self.anti_starvation_manager.registrar_tarea_ejecutada(is_low_prio)
                    if is_low_prio:
                        logging.debug(f"[ANTI-STARVATION] Executing low-priority task {m_id} (prio={prio})")

                # Check TTL expiration
                if not self.task_processor.check_ttl(t_arrival, ttl, m_id, corr_id):
                    self._register_incident("TTL_EXPIRED", task, "Expiration", m_id)
                    with self.lock:
                        self.global_metrics["ttl_deaths"] += 1
                    self.task_processor.record_ttl_expiration(m_id)
                    if not manual_extraction:
                        self.inbox.task_done()
                    continue

                self.state = "WORKING"
                t_start = time.time()
                
                # Register task start in metrics
                self.metrics_collector.registrar_inicio_tarea(m_id)

                # Build execution context
                context = {
                    "agent": self.name,
                    "task_id": m_id,
                    "priority": prio,
                    "complexity": comp,
                    "ttl": ttl,
                    "attempts": attempts,
                    "protocol": proto,
                    "correlation_id": corr_id,
                    "t_arrival": t_arrival,
                    "t_start": t_start,
                }

                # Execute task with circuit breaker
                def execute_work():
                    processed_task = self.middleware_stack.apply_before(task, context)
                    return self._technical_work(processed_task)
                
                success, result = self.task_processor.execute_task(
                    m_id,
                    task,
                    execute_work,
                    corr_id,
                    timeout_seconds=self.task_timeout_seconds,
                )
                
                if success:
                    processed_result = self.middleware_stack.apply_after(result, context)
                    # Task completed successfully
                    receipt = self._create_polyglot_receipt(
                        m_id, "COMPLETE", processed_result, t_arrival, t_start, proto, corr_id
                    )
                    self._dispatch_event("SUCCESS", receipt)
                    with self.lock:
                        self.global_metrics["successes"] += 1

                    self.lifecycle_hooks.invocar(
                        "on_task_completed",
                        {
                            **context,
                            "result": processed_result,
                            "status": "COMPLETE",
                            "timestamp": time.time(),
                        },
                    )
                    
                    self.task_processor.record_success(m_id, t_start, corr_id)
                    if not manual_extraction:
                        self.inbox.task_done()
                else:
                    # Task failed, apply error policy routing
                    error = result
                    error = self.middleware_stack.apply_error(error, context)
                    policy = (
                        self.error_policy_manager.obtener_politica(type(error).__name__)
                        if self.error_policy_manager
                        else PoliticaError.RETRY
                    )

                    should_retry = policy in [PoliticaError.RETRY, PoliticaError.BACKOFF] and \
                        self.task_processor.should_retry(error, attempts)

                    self.lifecycle_hooks.invocar(
                        "on_task_failed",
                        {
                            **context,
                            "error": str(error),
                            "error_type": type(error).__name__,
                            "policy": policy.value,
                            "timestamp": time.time(),
                        },
                    )

                    if policy == PoliticaError.DISCARD:
                        receipt = self._create_polyglot_receipt(
                            m_id, "ERROR_DISCARD", str(error), t_arrival, t_start, proto, corr_id
                        )
                        self._dispatch_event("INCIDENT", receipt)
                        with self.lock:
                            self.global_metrics["discarded"] += 1
                        self.task_processor.record_failure(m_id, error, corr_id)
                        if not manual_extraction:
                            self.inbox.task_done()

                    elif should_retry:
                        delay = self.task_processor.get_retry_delay(attempts)
                        if policy == PoliticaError.BACKOFF:
                            delay = delay * 2
                        if corr_id:
                            logging.debug(
                                f"[TRACE:{corr_id}] Retrying {m_id} in {delay:.2f}s (attempt {attempts+1}/{self.max_retries})"
                            )

                        self.lifecycle_hooks.invocar(
                            "on_retry",
                            {
                                **context,
                                "error": str(error),
                                "next_attempt": attempts + 1,
                                "delay": delay,
                                "timestamp": time.time(),
                            },
                        )

                        requeue_arrival = time.time() if self.renew_ttl_on_retry else t_arrival
                        self.task_processor.requeue_task(
                            self.inbox, prio, requeue_arrival, ttl, comp, attempts,
                            m_id, task, proto, corr_id, delay
                        )
                        with self.lock:
                            self.global_metrics["recovered_failures"] += 1
                        # Call task_done() for the current attempt (requeue creates new queue entry)
                        if not manual_extraction:
                            self.inbox.task_done()

                    else:
                        # DLQ or fatal failure
                        receipt = self._create_polyglot_receipt(
                            m_id, "ERROR_FATAL", str(error), t_arrival, t_start, proto, corr_id
                        )
                        self._dispatch_event("INCIDENT", receipt)
                        with self.lock:
                            self.dead_letter_queue.append(receipt)
                            self.global_metrics["fatal_failures"] += 1

                        self.task_processor.record_failure(m_id, error, corr_id)
                        if not manual_extraction:
                            self.inbox.task_done()

                self.state = "IDLE"
            except Empty:
                continue

    def _find_low_priority_task(self) -> Optional[tuple]:
        """Search inbox for lowest priority task (priority >= 3).
        
        Extracts all items from the queue, finds first low-priority task,
        and re-enqueues all non-selected items.
        
        IMPORTANT: Does NOT call task_done() - the caller is responsible
        for that after processing the returned task.
        
        Returns:
            Task tuple if found, None otherwise.
        """
        if self.inbox.empty():
            return None

        # Safely inspect and remove one low-priority item without touching unfinished_tasks
        with self.inbox.mutex:
            if not self.inbox.queue:
                return None

            low_index = None
            for idx, item in enumerate(self.inbox.queue):
                prio = item[0]
                if prio >= 3:
                    low_index = idx
                    break

            if low_index is None:
                return None

            low_prio_task = self.inbox.queue.pop(low_index)
            heapq.heapify(self.inbox.queue)
            return low_prio_task


    # --- ABSTRACT METRIC ---
    @abstractmethod
    def _technical_work(self, task):
        """This method MUST be implemented by the child agent."""
        raise NotImplementedError(
            "Child agent must define its own logic in _technical_work"
        )

    def _create_polyglot_receipt(self, m_id, status, result, t_arrival, t_start, proto, correlation_id=None):
        """Encapsulates response in original protocol with correlation ID."""
        safe_result = self._apply_payload_policy(result, self.max_result_bytes, "result")
        return self.protocol_adapter.create_receipt(
            m_id, status, safe_result, t_arrival, t_start, proto, correlation_id
        )

    def _payload_size_bytes(self, payload: Any) -> int:
        try:
            raw = json.dumps(payload, default=str)
        except Exception:
            raw = str(payload)
        return len(raw.encode("utf-8"))

    def _apply_payload_policy(self, payload: Any, max_bytes: Optional[int], kind: str) -> Any:
        if not max_bytes:
            return payload
        try:
            raw = json.dumps(payload, default=str)
        except Exception:
            raw = str(payload)
        size = len(raw.encode("utf-8"))
        if size <= max_bytes:
            return payload

        digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
        preview = raw[:256]
        policy = (self.payload_overflow_policy or "truncate").lower()

        marker = {
            "kind": kind,
            "original_bytes": size,
            "hash": digest,
            "preview": preview,
            "preview_bytes": len(preview.encode("utf-8")),
            "policy": policy,
        }

        if policy == "reject":
            marker["_rejected"] = True
            return marker
        if policy == "store_ref":
            marker["_stored_ref"] = True
            marker["ref"] = None
            return marker

        marker["_truncated"] = True
        return marker

    def _sanitize_event_data(self, event_type: str, data: Any) -> Any:
        if event_type == "INCIDENT" and isinstance(data, dict):
            if "task" in data:
                data = dict(data)
                data["task"] = self._apply_payload_policy(
                    data.get("task"), self.max_task_bytes, "task"
                )
            return data
        return data

    def _register_incident(self, incident_type, task, cause, m_id=None):
        """Centralizes error and rejection logging."""
        safe_task = self._apply_payload_policy(task, self.max_task_bytes, "task")
        data = {
            "type": incident_type,
            "mission_id": m_id,
            "task": safe_task,
            "cause": cause,
            "ts": time.ctime(),
        }
        self._dispatch_event("INCIDENT", data)
        if incident_type in ["SATURATION", "SECURITY", "SECURITY_SANDBOX", "RATE_LIMIT"]:
            with self.lock:
                self.global_metrics["rejected"] += 1

    # --- DIAGNOSTIC METHODS (Consistent with Tests) ---

    def get_operational_summary(self):
        """Returns agent operational metrics."""
        with self.lock:
            return {
                "agent": self.name,
                "current_state": self.state,
                "successful_tasks": self.global_metrics["successes"],
                "total_rejected_tasks": self.global_metrics["rejected"],
                "ttl_expired_tasks": self.global_metrics["ttl_deaths"],
                "successful_recoveries": self.global_metrics[
                    "recovered_failures"
                ],
                "fatal_failures": self.global_metrics["fatal_failures"],
                "discarded_tasks": self.global_metrics["discarded"],
                "pending_tasks": self.inbox.qsize(),
            }

    def get_mission_details(self, category="successes", limit=None):
        """Returns detailed in-memory lists.

        Args:
            category: "successes", "incidents" or "queue".
            limit: maximum number of elements to return (from the end).
        """
        with self.lock:
            if category == "successes":
                if limit is not None:
                    return list(deque(self.outbox, maxlen=limit))
                return list(self.outbox)
            if category == "incidents":
                if limit is not None:
                    return list(deque(self.incident_log, maxlen=limit))
                return list(self.incident_log)
            if category == "queue":
                if limit is not None:
                    return [t[-1] for t in list(deque(self.inbox.queue, maxlen=limit))]
                return [t[-1] for t in list(self.inbox.queue)]
            if category == "dead_letter":
                if limit is not None:
                    return list(deque(self.dead_letter_queue, maxlen=limit))
                return list(self.dead_letter_queue)
            return {"error": "Invalid category"}

    # --- ENTERPRISE FEATURES API ---
    
    def get_correlation(self, correlation_id: str) -> Dict[str, Any]:
        """Gets correlation status and delegation chain.
        
        Args:
            correlation_id: Correlation ID to track.
        
        Returns:
            Dictionary with metadata and delegation chain.
        """
        return self.correlation_tracker.get_metadata(correlation_id)
    
    def get_circuit_state(self) -> str:
        """Gets current circuit breaker state.
        
        Returns:
            "closed", "open", or "half_open", or "disabled" if not enabled.
        """
        return self.circuit_breaker.get_state() if self.circuit_breaker else "disabled"
    
    def get_retry_strategy(self) -> str:
        """Gets retry strategy class in use.
        
        Returns:
            Strategy class name.
        """
        return self.retry_strategy.__class__.__name__

    def monitor_health(self):
        """Quick vitality status."""
        inactivity = time.time() - self.last_activity
        return {
            "agent": self.name,
            "state": self.state,
            "inactive_for": round(inactivity, 2),
            "circuit_breaker": self.get_circuit_state(),
            "retry_strategy": self.get_retry_strategy(),
        }

    # --- SYSTEM SHUTDOWN ---

    def shutdown(self, timeout: float = 30.0) -> Dict[str, Any]:
        """Deprecated: use graceful_shutdown().

        This is now a safe alias to graceful_shutdown to avoid non-graceful stops.
        """
        logging.warning(
            "[DEPRECATED] shutdown() is deprecated. Use graceful_shutdown() instead."
        )
        return self.graceful_shutdown(timeout=timeout)

    def send_to_agent(self, target_agent, message, priority=2):
        """Allows assigning a task to another fleet agent."""
        logging.info(f"🔄 {self.name} delegating task to {target_agent.name}")
        return target_agent.receive_message(message, priority=priority)

    def _perception_engine(self):
        while self.alive:
            # If queue is very full, wait to avoid saturation
            logging.debug(
                "Agent %s: perception cycle", self.name
            )
            if self.inbox.qsize() < (self.capacity / 2):
                try:
                    self.perceive_environment()
                except Exception as e:
                    logging.error("Error in perception: %s", e)

            # Emit metrics snapshot hook
            try:
                snapshot = self.metrics_collector.obtener_estado()
                self.lifecycle_hooks.invocar(
                    "on_metrics_snapshot",
                    {
                        "agent": self.name,
                        "timestamp": time.time(),
                        "metrics": snapshot,
                    },
                )
            except Exception as e:
                logging.error("Error in metrics snapshot hook: %s", e)

            # "Go to sleep" logic if no activity
            if self.state == "IDLE":
                time.sleep(self.perception_frequency * 2)  # Sleep more if idle
            else:
                time.sleep(self.perception_frequency)

    def perceive_environment(self):
        """Optional method: child decides HOW to look at the world."""
        return None

    @property
    def last_activity(self):
        with self.lock:
            return self._last_activity

    @last_activity.setter
    def last_activity(self, value):
        with self.lock:
            self._last_activity = value    
    # --- LIFECYCLE HOOKS ---
    
    def register_hook(self, event: str, callback: Callable):
        """Registers callback for lifecycle event.
        
        Available events:
            - on_start: Agent started
            - on_stop: Agent stopped
            - on_error: Unrecoverable error
            - on_task_received: Task accepted into queue
            - on_task_completed: Task completed
            - on_retry: Task retry scheduled
            - on_metrics_snapshot: Periodic metrics snapshot
        """
        try:
            self.lifecycle_hooks.registrar(event, callback)
        except ValueError as e:
            logging.error(f"Error registering hook: {e}")

    def add_middleware(self, middleware: MiddlewarePlugin):
        """Registers a middleware plugin for task processing pipeline."""
        try:
            self.middleware_stack.add(middleware)
        except ValueError as e:
            logging.error(f"Error registering middleware: {e}")
    
    def invoke_custom_hook(self, hook_name: str, *args, **kwargs):
        """Invokes registered hooks (internal use)."""
        self.lifecycle_hooks.invocar(hook_name, *args, **kwargs)
    
    def configure_rate_limit(self, token_rate: float, max_capacity: float):
        """Configures global rate limiter."""
        if self.rate_limiter:
            self.rate_limiter.tasa_tokens = token_rate
            self.rate_limiter.capacidad_max = max_capacity
    
    def add_origin_limit(self, origin: str, token_rate: float, max_capacity: float):
        """Adds specific limit for an origin."""
        if self.rate_limiter:
            self.rate_limiter.registrar_limite_origen(origin, token_rate, max_capacity)
    
    def get_rate_limiter_state(self) -> Dict[str, Any]:
        """Returns rate limiter state."""
        return self.rate_limiter.obtener_estado() if self.rate_limiter else {}
    
    def get_metrics(self) -> Dict[str, Any]:
        """Returns performance metrics."""
        return self.metrics_collector.obtener_estado()
    
    def get_avg_latency(self) -> float:
        """Returns average latency in seconds."""
        return self.metrics_collector.obtener_latencia_promedio()
    
    def get_p95_latency(self) -> float:
        """Returns 95th percentile latency."""
        return self.metrics_collector.obtener_latencia_p95()
    
    def get_throughput(self) -> float:
        """Returns throughput: tasks per second."""
        return self.metrics_collector.obtener_throughput()
    
    def get_error_rate(self) -> float:
        """Returns error rate (0.0 to 1.0)."""
        return self.metrics_collector.obtener_tasa_error()
    
    def clear_metrics(self):
        """Clears all collected metrics."""
        self.metrics_collector.limpiar()
    
    def change_queue_policy(self, policy: PoliticaCola):
        """Changes queue dispatch policy."""
        self.queue_policy_manager.cambiar_politica(policy)
    
    def get_queue_policy(self) -> str:
        """Returns current queue policy."""
        return self.queue_policy_manager.politica.value
    
    def get_audit_logs(self, last_n: int = 100) -> list:
        """Returns last N audit records."""
        return self.auditor.obtener_registros(last_n) if self.auditor else []
    
    def get_audit_logs_by_event(self, event: str) -> list:
        """Returns audit records filtered by event."""
        return self.auditor.obtener_registros_por_evento(event) if self.auditor else []
    
    def export_audit_json(self) -> str:
        """Exports audit as JSON."""
        return self.auditor.exportar_json() if self.auditor else "{}"
    
    def configure_default_error_policy(self, policy: PoliticaError):
        """Configures default error policy."""
        self.error_policy_manager.politica_defecto = policy
        logging.info(f"Default error policy: {policy.value}")
    
    def register_error_policy_by_type(self, error_type: str, policy: PoliticaError):
        """Registers specific policy for error type."""
        self.error_policy_manager.registrar_politica_por_tipo(error_type, policy)
    
    def get_error_policy(self, error_type: str) -> str:
        """Returns configured policy for an error type."""
        policy = self.error_policy_manager.obtener_politica(error_type)
        return policy.value
    
    def get_anti_starvation_stats(self) -> Dict[str, Any]:
        """Returns starvation protection statistics."""
        return self.anti_starvation_manager.obtener_estadisticas() if self.anti_starvation_manager else {}
    
    def set_min_low_priority_ratio(self, ratio: float):
        """Sets minimum ratio for low priority tasks (0.0 to 1.0)."""
        if self.anti_starvation_manager:
            ratio = max(0.0, min(1.0, ratio))
            self.anti_starvation_manager.ratio_minimo = ratio
            logging.info(f"Minimum low priority ratio set: {ratio}")
    
    def clear_anti_starvation_stats(self):
        """Clears anti-starvation manager statistics."""
        if self.anti_starvation_manager:
            self.anti_starvation_manager.limpiar()
    
    def add_denylist_pattern(self, pattern: str):
        """Adds forbidden pattern to security sandbox."""
        if self.security_sandbox:
            self.security_sandbox.agregar_denylist(pattern)

    def add_denylist_regex(self, pattern: str):
        """Adds forbidden regex pattern to security sandbox."""
        if self.security_sandbox:
            self.security_sandbox.agregar_denylist_regex(pattern)
    
    def add_whitelist_pattern(self, pattern: str):
        """Adds allowed pattern to security sandbox."""
        if self.security_sandbox:
            self.security_sandbox.agregar_whitelist(pattern)

    def add_whitelist_regex(self, pattern: str):
        """Adds allowed regex pattern to security sandbox."""
        if self.security_sandbox:
            self.security_sandbox.agregar_whitelist_regex(pattern)
    
    def validate_safe_task(self, task: str) -> bool:
        """Validates if a task is safe according to sandbox policies."""
        return self.security_sandbox.es_segura_tarea(task) if self.security_sandbox else True
    
    def get_security_stats(self) -> Dict[str, Any]:
        """Gets security validation statistics."""
        return self.security_sandbox.obtener_estadisticas_seguridad() if self.security_sandbox else {}
    
    def enable_whitelist_mode(self, enable: bool = True):
        """Enables or disables whitelist mode in sandbox."""
        if self.security_sandbox:
            self.security_sandbox.habilitar_whitelist = enable
            mode = "whitelist" if enable else "denylist"
            logging.info(f"Sandbox mode changed to: {mode}")
    
    def clear_security_stats(self):
        """Clears security counters."""
        if self.security_sandbox:
            self.security_sandbox.limpiar_estadisticas()
    
    def liveness_probe(self) -> Dict[str, Any]:
        """Gets agent liveness state (is it alive?)."""
        return self.health_probe.sondeo_liveness() if self.health_probe else {"status": "healthy", "message": "Health probes disabled"}
    
    def readiness_probe(self) -> Dict[str, Any]:
        """Gets agent readiness state (is it ready?)."""
        if not self.health_probe:
            return {"status": "ready", "message": "Health probes disabled"}
        return self.health_probe.sondeo_readiness(
            self.alive,
            self.inbox.qsize(),
            self.capacity
        )
    
    def startup_probe(self) -> Dict[str, Any]:
        """Gets agent startup state (did it finish initialization?)."""
        return self.health_probe.sondeo_startup(self.alive) if self.health_probe else {"status": "started", "message": "Health probes disabled"}
    
    def get_general_health_state(self) -> Dict[str, Any]:
        """Gets general health state of agent."""
        return self.health_probe.obtener_estado_general(
            self.alive,
            self.inbox.qsize(),
            self.capacity
        )
    
    def graceful_shutdown(self, timeout: float = 30.0) -> Dict[str, Any]:
        """
        Stops agent gracefully (graceful shutdown).
        
        Steps:
        1. DRAINING: Stop accepting tasks, drain queue with active worker
        2. STOPPING: Signal stop (alive=False)
        3. Wait for threads to finish
        4. Save final state
        5. Clean resources
        
        Args:
            timeout: Maximum seconds to wait (default 30s)
        
        Returns:
            Dict with shutdown statistics
        """
        logging.info(f"[SHUTDOWN] {self.name}: Entering DRAINING state...")
        shutdown_start = time.time()
        self.shutting_down = True
        
        # === STATE 1→2: DRAINING ===
        # Block new task ingress while worker continues processing
        self.accepting_tasks = False
        
        drain_start = time.time()
        drain_timeout = timeout * 0.7  # Use 70% of timeout for draining
        while not self.inbox.empty() and (time.time() - drain_start) < drain_timeout:
            remaining = self.inbox.qsize()
            if remaining > 0 and remaining % 10 == 0:
                logging.info(
                    f"[SHUTDOWN] {self.name}: Draining... {remaining} tasks remaining"
                )
            time.sleep(0.1)  # Worker continues processing
        
        # === STATE 2→3: STOPPING ===
        logging.info(f"[SHUTDOWN] {self.name}: Entering STOPPING state...")
        self.alive = False  # Signal stop AFTER draining
        
        # Delegate to agent_runtime for lifecycle management
        remaining_timeout = timeout - (time.time() - shutdown_start)
        stats = self.agent_runtime.graceful_shutdown(
            self.inbox,
            timeout=remaining_timeout,
            metrics=self.global_metrics,
            outbox=self.outbox,
            incident_log=self.incident_log,
            health_probe=self.health_probe,
            auditor=self.auditor,
            metrics_collector=self.metrics_collector,
            alive_flag_getter=lambda: self.alive,
            capacity=self.capacity
        )
        
        # Add dead_letter_queue size to stats
        stats["dead_letter_size"] = len(self.dead_letter_queue)
        
        logging.info(f"[SHUTDOWN] {self.name}: Shutdown completed in {stats['shutdown_seconds']}s")
        return stats