# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

"""Agent status emitter — adapter layer for agent lifecycle status emission.

Wraps ModelAgentStatusPayload and emits via the emit daemon.
This is a thin wrapper over emit_event() following the established
omniclaude emit daemon pattern (hook → socket → daemon → Kafka).

INVARIANT: This function MUST fail open and NEVER block agent execution.
If Kafka/daemon is unavailable, log warning and return False.

Related Tickets:
    - OMN-1848: Agent Status Kafka Emitter (fail-open)
    - OMN-1847: EnumAgentState + ModelAgentStatus (blocker, models defined locally)

.. versionadded:: 0.3.0
"""

from __future__ import annotations

import logging
import os
import threading
from datetime import UTC, datetime
from uuid import UUID, uuid4

logger = logging.getLogger(__name__)


def emit_agent_status(
    state: str,
    message: str,
    *,
    correlation_id: UUID | None = None,
    agent_name: str | None = None,
    session_id: str | None = None,
    agent_instance_id: str | None = None,
    progress: float | None = None,
    current_phase: str | None = None,
    current_task: str | None = None,
    blocking_reason: str | None = None,
    metadata: dict[str, str] | None = None,
) -> bool:
    """Emit agent status event via the emit daemon (non-blocking, fail-open).

    Validates input via ModelAgentStatusPayload (frozen Pydantic model),
    serializes to dict, and sends through the emit daemon to Kafka.

    INVARIANT: This function MUST fail open and NEVER block agent execution.

    Args:
        state: Agent state string (must be valid EnumAgentState value:
            idle, working, blocked, awaiting_input, finished, error).
            Accepts both str and EnumAgentState.
        message: Human-readable status message (max 500 chars).
        correlation_id: Correlation ID for tracing (generated by emitter if None).
        agent_name: Name of the agent (defaults to AGENT_NAME env var).
        session_id: Session ID (defaults to SESSION_ID env var).
        agent_instance_id: Durable instance ID for parallel agent disambiguation.
        progress: Progress 0.0-1.0 (optional, monotonic within task).
        current_phase: Current workflow phase (optional).
        current_task: Current task description (optional).
        blocking_reason: Why blocked (optional, for notifications).
        metadata: Additional metadata dict (optional).

    Returns:
        True if successfully emitted to daemon socket, False otherwise.
        Note: True means accepted by daemon, not delivered to Kafka.
    """
    try:
        from omniclaude.hooks.schemas import EnumAgentState, ModelAgentStatusPayload

        # Coerce string state to enum (fail-open on invalid)
        try:
            validated_state = EnumAgentState(state)
        except ValueError:
            valid_states = [s.value for s in EnumAgentState]
            # Use explicit None check so empty strings are preserved as-is
            # "unknown" sentinel indicates caller did not provide value; see event_registry.py
            _log_agent = (
                agent_name
                if agent_name is not None
                else os.environ.get("AGENT_NAME", "unknown")
            )
            _log_session = (
                session_id
                if session_id is not None
                else os.environ.get("SESSION_ID", "unknown")
            )
            logger.error(
                "Invalid agent state: %r (valid: %s), event_type=agent.status, "
                "agent_name=%s, session_id=%s",
                state,
                valid_states,
                _log_agent,
                _log_session,
            )
            return False

        # Resolve defaults from environment (explicit None check preserves empty strings)
        # "unknown" sentinel indicates caller did not provide value; see event_registry.py
        resolved_agent_name = (
            agent_name
            if agent_name is not None
            else os.environ.get("AGENT_NAME", "unknown")
        )
        # "unknown" sentinel indicates caller did not provide value; see event_registry.py
        resolved_session_id = (
            session_id
            if session_id is not None
            else os.environ.get("SESSION_ID", "unknown")
        )

        # Track data quality degradation from sentinel defaults (OMN-6907)
        _degraded_fields: list[str] = []
        if resolved_agent_name == "unknown":
            logger.warning(
                "agent_name resolved to 'unknown' — AGENT_NAME env var not set and no --agent-name provided"
            )
            _degraded_fields.append("agent_name")
        if resolved_session_id == "unknown":
            logger.warning(
                "session_id resolved to 'unknown' — SESSION_ID env var not set and no --session-id provided"
            )
            _degraded_fields.append("session_id")

        # Build validated payload — Pydantic enforces all constraints
        # correlation_id is always required (no default on the model);
        # generate here if the caller didn't supply one, consistent with
        # how emitted_at is explicitly injected rather than defaulted.
        resolved_correlation_id = (
            correlation_id if correlation_id is not None else uuid4()
        )

        # Merge caller metadata with data quality tags so downstream
        # consumers can filter/flag events with sentinel defaults.
        resolved_metadata = dict(metadata) if metadata else {}
        if _degraded_fields:
            resolved_metadata["data_quality"] = "degraded"
            resolved_metadata["degraded_fields"] = ",".join(_degraded_fields)

        payload_model = ModelAgentStatusPayload(
            correlation_id=resolved_correlation_id,
            agent_name=resolved_agent_name,
            session_id=resolved_session_id,
            agent_instance_id=agent_instance_id,
            state=validated_state,
            message=message,
            progress=progress,
            current_phase=current_phase,
            current_task=current_task,
            blocking_reason=blocking_reason,
            emitted_at=datetime.now(UTC),
            metadata=resolved_metadata,
            task_id=os.getenv("ONEX_TASK_ID"),
        )

        # Serialize to flat dict for daemon transport
        payload = payload_model.model_dump(mode="json")

        from .emit_client_wrapper import emit_event

        result = emit_event("agent.status", payload)

        # Trigger Slack notification for blocked state (OMN-1851)
        # Only notify when emit_event succeeds (P0-6)
        # Runs in a daemon thread to avoid blocking the hook path.
        if result and validated_state == EnumAgentState.BLOCKED:
            try:
                from .blocked_notifier import maybe_notify_blocked

                threading.Thread(
                    target=maybe_notify_blocked,
                    args=(payload,),
                    daemon=True,
                ).start()
            except Exception:
                pass  # Notification failure must never affect status emission

        return result

    except Exception as e:
        # Structured failure log for debugging (explicit None check preserves empty strings)
        logger.warning(
            "Failed to emit agent status: error_class=%s, error_message=%.200s, "
            "event_type=agent.status, agent_name=%s, session_id=%s, schema_version=1",
            type(e).__name__,
            str(e),
            # "unknown" sentinel indicates caller did not provide value; see event_registry.py
            agent_name
            if agent_name is not None
            else os.environ.get("AGENT_NAME", "unknown"),
            # "unknown" sentinel indicates caller did not provide value; see event_registry.py
            session_id
            if session_id is not None
            else os.environ.get("SESSION_ID", "unknown"),
        )
        return False


__all__ = [
    "emit_agent_status",
]
