# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
"""Service wiring for omniclaude handlers.

This module publishes handler contracts to Kafka for discovery by the platform's
KafkaContractSource. Handler instantiation is handled by omnibase_infra's
ServiceRuntimeHostProcess.

Ticket: OMN-1812 - Migrate wiring.py to use ServiceContractPublisher from omnibase_infra

Architecture (ARCH-002):
    Runtime owns all Kafka plumbing. This module delegates to ServiceContractPublisher
    from omnibase_infra which handles:
    - YAML parsing and validation
    - Contract hash computation
    - Kafka event emission (ModelContractRegisteredEvent)
    - Error categorization (contract vs infrastructure)

Event-Driven Wiring Strategy:
    1. Read handler contracts from configured source (filesystem or package)
    2. Emit ModelContractRegisteredEvent to Kafka for each handler
    3. KafkaContractSource (in omnibase_infra) caches the descriptors
    4. ServiceRuntimeHostProcess imports, instantiates, and initializes handlers

This replaces the previous local implementation with the standardized infra service.

Contract Discovery — Skill Nodes (OMN-2401 / OMN-2402):
    ServiceContractPublisher uses ``root.glob("**/contract.yaml")`` to discover
    contracts from the configured ``OMNICLAUDE_CONTRACTS_ROOT``.  This recursive
    glob **already covers** all skill node contracts generated by the
    ``generate_skill_node.py`` script (OMN-2401), which land at:

        src/omniclaude/nodes/node_skill_{snake}_orchestrator/contract.yaml

    No code change is required.  When ``OMNICLAUDE_CONTRACTS_ROOT`` is set to
    the ``src/omniclaude/nodes/`` directory (or any parent that contains it),
    skill node contracts are loaded automatically at plugin startup — exactly
    like all other node contracts.

    Verification (run from repo root):

        python3 -c \"
        import yaml
        from pathlib import Path
        for f in sorted(Path('src/omniclaude/nodes').glob('**/contract.yaml')):
            data = yaml.safe_load(f.read_text())
            name = data.get('name', '')
            print(f'OK: {name}')
        \"
"""

from __future__ import annotations

import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING

from omnibase_infra.services.contract_publisher import (
    ContractSourceNotConfiguredError,
    ModelContractPublisherConfig,
    ModelPublishResult,
    ServiceContractPublisher,
)

if TYPE_CHECKING:
    from omnibase_core.models.container.model_onex_container import ModelONEXContainer

logger = logging.getLogger(__name__)


async def publish_handler_contracts(
    container: ModelONEXContainer,
    config: ModelContractPublisherConfig,
    environment: str | None = None,
) -> ModelPublishResult:
    """Publish handler contracts to Kafka for discovery.

    Emits ModelContractRegisteredEvent for each handler contract found in
    the configured source. The events are consumed by KafkaContractSource
    in omnibase_infra for handler discovery and registration.

    This function delegates to ServiceContractPublisher from omnibase_infra
    which handles all YAML parsing, validation, and Kafka emission.

    Error Handling:
        - Contract errors (YAML parse, schema validation, missing fields) are
          non-fatal: logged, added to contract_errors, processing continues.
        - Infrastructure errors (Kafka, publisher) are fatal by default:
          raises ContractPublishingInfraError immediately when fail_fast=True.
        - Zero contracts raises NoContractsFoundError when allow_zero_contracts=False
          and no contract files exist. If contracts are found but all fail validation,
          the function returns normally with contract_errors populated.

    Args:
        container: The ONEX container with event bus publisher.
        config: Configuration specifying contract source (filesystem, package, composite).
        environment: Environment label passed through to omnibase_infra's
            ServiceContractPublisher for contract registration metadata.
            Per OMN-1972, this is no longer used as a Kafka topic prefix by
            omniclaude. Per OMN-5189, ONEX_ENV is vestigial — the infra layer
            defaults to "local" via ONEX_ENVIRONMENT. Empty string means no env
            prefix (canonical topics, which is the standard).

    Returns:
        ModelPublishResult with published handler IDs, contract_errors, infra_errors,
        and duration_ms.

    Raises:
        ImportError: If omnibase_infra contract publisher is not available.
        NotImplementedError: If package or composite mode is used (not yet implemented).
        NoContractsFoundError: If no contract files found (empty directory) and
            allow_zero_contracts=False. Not raised if contracts exist but all fail validation.
        ContractPublishingInfraError: If infrastructure errors occur and fail_fast=True.

    Example:
        >>> container = ModelONEXContainer(...)
        >>> config = ModelContractPublisherConfig(
        ...     mode="filesystem",
        ...     filesystem_root=Path("/app/contracts/handlers"),
        ... )
        >>> result = await publish_handler_contracts(container, config)
        >>> print(f"Published {len(result.published)} contracts in {result.duration_ms:.1f}ms")
        >>> if result.contract_errors:
        ...     print(f"Errors: {len(result.contract_errors)}")
    """
    # Resolve environment if provided (store in config; from_container does not accept it)
    if environment is not None:
        resolved_env = environment.strip()
        config = config.model_copy(update={"environment": resolved_env})

    # Delegate to infra service — environment is embedded in config, not a kwarg
    publisher = await ServiceContractPublisher.from_container(container, config)
    return await publisher.publish_all()


async def wire_omniclaude_services(
    container: ModelONEXContainer,
    config: ModelContractPublisherConfig | None = None,
) -> None:
    """Register omniclaude handlers with the platform via event-driven discovery.

    This function publishes handler contracts to Kafka. The actual handler
    instantiation is handled by ServiceRuntimeHostProcess in omnibase_infra
    when it receives the registration events via KafkaContractSource.

    After publishing, handlers can be resolved via:
        handler = await container.get_service_async(ProtocolPatternPersistence)

    Note: Handler availability depends on KafkaContractSource caching the
    registration event. In beta, this requires a runtime restart after
    first publication.

    Args:
        container: The ONEX container with event bus publisher.
        config: Configuration specifying contract source. If None, falls back
            to OMNICLAUDE_CONTRACTS_ROOT environment variable.

    Raises:
        ContractSourceNotConfiguredError: If no config provided and
            OMNICLAUDE_CONTRACTS_ROOT environment variable is not set.

    Example:
        from omniclaude.runtime import wire_omniclaude_services
        from omnibase_infra.services.contract_publisher import ModelContractPublisherConfig
        from pathlib import Path

        # During application bootstrap with explicit config
        container = ModelONEXContainer(...)
        config = ModelContractPublisherConfig(
            mode="filesystem",
            filesystem_root=Path("/app/contracts/handlers"),
        )
        await wire_omniclaude_services(container, config)

        # Or using environment variable
        # export OMNICLAUDE_CONTRACTS_ROOT=/app/contracts/handlers
        await wire_omniclaude_services(container)
    """
    if config is None:
        # Check for env var configuration
        contracts_root = os.getenv("OMNICLAUDE_CONTRACTS_ROOT")
        if contracts_root is None:
            raise ContractSourceNotConfiguredError(
                mode="env_var",
                missing_field="OMNICLAUDE_CONTRACTS_ROOT",
            )
        # allow_zero_contracts=True: omniclaude ships node contracts (ONEX
        # contract.yaml), not handler contracts (ModelHandlerContract).  The
        # ServiceContractPublisher validates against ModelHandlerContract which
        # will reject all node contracts.  Setting allow_zero_contracts=True
        # prevents a NoContractsFoundError crash while still logging per-
        # contract validation errors for diagnostics.
        # Ticket: OMN-3906
        config = ModelContractPublisherConfig(
            mode="filesystem",
            filesystem_root=Path(contracts_root),
            allow_zero_contracts=True,
        )

    result = await publish_handler_contracts(container, config)

    # Log diagnostic summary so that validation failures are visible
    # without requiring code-level debugging.
    if result.contract_errors:
        logger.warning(
            "Contract validation: %d/%d contracts failed ModelHandlerContract "
            "validation (expected for node contracts). First error: %s",
            len(result.contract_errors),
            len(result.contract_errors) + len(result.published),
            result.contract_errors[0].message if result.contract_errors else "N/A",
        )
    if result.published:
        logger.info(
            "Published %d handler contracts to Kafka",
            len(result.published),
        )
