"""Event hook system for opendeviationbar-py operations.

This module provides a publish-subscribe event system for monitoring
long-running cache operations, validation results, and population progress.

Usage
-----
>>> from opendeviationbar.hooks import register_hook, HookEvent
>>>
>>> def my_callback(payload):
...     print(f"Event: {payload.event.value}, Symbol: {payload.symbol}")
...
>>> register_hook(HookEvent.CACHE_WRITE_COMPLETE, my_callback)
>>> # Later, when bars are cached:
>>> # Event: cache_write_complete, Symbol: BTCUSDT

Events
------
- CACHE_WRITE_START: Cache write operation started
- CACHE_WRITE_COMPLETE: Cache write operation completed successfully
- CACHE_WRITE_FAILED: Cache write operation failed
- VALIDATION_COMPLETE: Post-storage validation passed
- VALIDATION_FAILED: Post-storage validation failed
- CHECKPOINT_SAVED: Resumable population checkpoint saved
- POPULATION_COMPLETE: Cache population completed successfully
- POPULATION_FAILED: Cache population failed
"""

from __future__ import annotations

import json
import logging
import uuid
from collections import defaultdict
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from enum import Enum
from typing import Any, Callable

logger = logging.getLogger(__name__)


class HookEvent(Enum):
    """Events that can trigger hook callbacks.

    Attributes
    ----------
    CACHE_WRITE_START : str
        Emitted when cache write operation begins.
    CACHE_WRITE_COMPLETE : str
        Emitted when cache write completes successfully.
    CACHE_WRITE_FAILED : str
        Emitted when cache write fails.
    VALIDATION_COMPLETE : str
        Emitted when post-storage validation passes.
    VALIDATION_FAILED : str
        Emitted when post-storage validation fails.
    CHECKPOINT_SAVED : str
        Emitted when a population checkpoint is saved.
    POPULATION_COMPLETE : str
        Emitted when cache population completes.
    POPULATION_FAILED : str
        Emitted when cache population fails.
    """

    CACHE_WRITE_START = "cache_write_start"
    CACHE_WRITE_COMPLETE = "cache_write_complete"
    CACHE_WRITE_FAILED = "cache_write_failed"
    VALIDATION_COMPLETE = "validation_complete"
    VALIDATION_FAILED = "validation_failed"
    CHECKPOINT_SAVED = "checkpoint_saved"
    POPULATION_COMPLETE = "population_complete"
    POPULATION_FAILED = "population_failed"
    # Issue #48: Diagnostic tracing events
    DOWNLOAD_START = "download_start"
    DOWNLOAD_COMPLETE = "download_complete"
    TICK_FETCH_PROGRESS = "tick_fetch_progress"
    MIGRATION_PROGRESS = "migration_progress"
    # Strategy/Backtest telemetry events
    STRATEGY_INIT = "strategy_init"
    STRATEGY_SIGNAL = "strategy_signal"
    TRADE_COMPLETE = "trade_complete"
    BACKTEST_COMPLETE = "backtest_complete"
    # Issue #97: Checkpoint lifecycle events
    CHECKPOINT_RESTORED = "checkpoint_restored"
    CHECKPOINT_RESTORE_FAILED = "checkpoint_restore_failed"
    SIDECAR_CHECKPOINT_SAVED = "sidecar_checkpoint_saved"
    SIDECAR_CHECKPOINT_RESTORED = "sidecar_checkpoint_restored"
    # Issue #98: Plugin feature enrichment events
    PLUGIN_ENRICH_COMPLETE = "plugin_enrich_complete"
    PLUGIN_ENRICH_FAILED = "plugin_enrich_failed"
    # Issue #117-119: Sidecar reliability + monitoring events
    SYMBOL_COVERAGE_MISMATCH = "symbol_coverage_mismatch"
    SIDECAR_STARTED = "sidecar_started"
    SIDECAR_STOPPED = "sidecar_stopped"
    KINTSUGI_PASS_COMPLETE = "kintsugi_pass_complete"
    GAP_DETECTED = "gap_detected"


@dataclass
class HookPayload:
    """Payload delivered to hook callbacks.

    Attributes
    ----------
    event : HookEvent
        The event type that triggered this callback.
    symbol : str
        Trading symbol involved (e.g., "BTCUSDT").
    timestamp : datetime
        When the event occurred (UTC).
    details : dict
        Additional event-specific information.
    is_failure : bool
        True if this event represents a failure.
    """

    event: HookEvent
    symbol: str
    timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
    details: dict[str, Any] = field(default_factory=dict)
    is_failure: bool = False
    trace_id: str | None = None

    def to_dict(self) -> dict[str, Any]:
        """Convert payload to dictionary for serialization."""
        d = asdict(self)
        d["event"] = self.event.value
        d["timestamp"] = self.timestamp.isoformat()
        return d

    def to_json(self) -> str:
        """Convert payload to JSON string."""
        return json.dumps(self.to_dict(), indent=2)


# Global hook registry
_hooks: dict[HookEvent, list[Callable[[HookPayload], None]]] = defaultdict(list)


def register_hook(
    event: HookEvent,
    callback: Callable[[HookPayload], None],
) -> None:
    """Register a callback for a specific event.

    Parameters
    ----------
    event : HookEvent
        The event to subscribe to.
    callback : Callable[[HookPayload], None]
        Function to call when the event occurs. Receives a HookPayload.

    Examples
    --------
    >>> def log_completion(payload):
    ...     print(f"Completed: {payload.symbol} at {payload.timestamp}")
    ...
    >>> register_hook(HookEvent.CACHE_WRITE_COMPLETE, log_completion)
    """
    _hooks[event].append(callback)
    logger.debug("Registered hook for %s: %s", event.value, callback.__name__)


def unregister_hook(
    event: HookEvent,
    callback: Callable[[HookPayload], None],
) -> bool:
    """Unregister a callback for a specific event.

    Parameters
    ----------
    event : HookEvent
        The event to unsubscribe from.
    callback : Callable[[HookPayload], None]
        The callback to remove.

    Returns
    -------
    bool
        True if the callback was found and removed, False otherwise.
    """
    if callback in _hooks[event]:
        _hooks[event].remove(callback)
        logger.debug("Unregistered hook for %s: %s", event.value, callback.__name__)
        return True
    return False


def clear_hooks(event: HookEvent | None = None) -> None:
    """Clear all hooks for a specific event or all events.

    Parameters
    ----------
    event : HookEvent | None
        Event to clear hooks for. If None, clears all hooks.
    """
    if event is None:
        _hooks.clear()
        logger.debug("Cleared all hooks")
    else:
        _hooks[event].clear()
        logger.debug("Cleared hooks for %s", event.value)


def emit_hook(
    event: HookEvent,
    symbol: str,
    *,
    trace_id: str | None = None,
    **details: Any,
) -> None:
    """Emit an event to all registered callbacks.

    Parameters
    ----------
    event : HookEvent
        The event type to emit.
    symbol : str
        Trading symbol involved.
    **details
        Additional event-specific information.

    Notes
    -----
    Callback exceptions are caught and logged but do not propagate.
    This ensures one failing callback doesn't break the main operation.

    Examples
    --------
    >>> emit_hook(
    ...     HookEvent.CACHE_WRITE_COMPLETE,
    ...     symbol="BTCUSDT",
    ...     bars_written=1500,
    ...     threshold_bps=250,
    ... )
    """
    # Add memory snapshot to all hook payloads (Issue #49 T2.3)
    try:
        from opendeviationbar.resource_guard import get_memory_info

        mem = get_memory_info()
        details["memory_rss_mb"] = mem.process_rss_mb
        details["memory_pct"] = round(mem.usage_pct, 3)
    except (ImportError, OSError, AttributeError):
        logger.debug("Memory snapshot unavailable for hook payload", exc_info=True)

    is_failure = "FAILED" in event.name
    payload = HookPayload(
        event=event,
        symbol=symbol,
        timestamp=datetime.now(UTC),
        details=details,
        is_failure=is_failure,
        trace_id=trace_id,
    )

    callbacks = _hooks.get(event, [])
    if not callbacks:
        logger.debug("No hooks registered for %s", event.value)
        return

    logger.debug(
        "Emitting %s for %s to %d callback(s)",
        event.value,
        symbol,
        len(callbacks),
    )

    for callback in callbacks:
        try:
            callback(payload)
        except (OSError, RuntimeError, ValueError, TypeError) as e:
            # Log but don't propagate - callbacks shouldn't break main flow
            logger.warning(
                "Hook callback %s failed for %s: %s",
                callback.__name__,
                event.value,
                e,
            )


def register_for_failures(
    callback: Callable[[HookPayload], None],
) -> None:
    """Register a callback for all failure events.

    Convenience function to subscribe to all *_FAILED events.

    Parameters
    ----------
    callback : Callable[[HookPayload], None]
        Function to call when any failure event occurs.

    Examples
    --------
    >>> def alert_on_failure(payload):
    ...     send_slack_alert(f"FAILURE: {payload.event.value}")
    ...
    >>> register_for_failures(alert_on_failure)
    """
    for event in HookEvent:
        if "FAILED" in event.name:
            register_hook(event, callback)


def register_for_all(
    callback: Callable[[HookPayload], None],
) -> None:
    """Register a callback for all events.

    Parameters
    ----------
    callback : Callable[[HookPayload], None]
        Function to call when any event occurs.

    Examples
    --------
    >>> def log_all(payload):
    ...     print(f"{payload.event.value}: {payload.symbol}")
    ...
    >>> register_for_all(log_all)
    """
    for event in HookEvent:
        register_hook(event, callback)


def generate_trace_id() -> str:
    """Generate a short unique trace ID for correlating hook events.

    Returns
    -------
    str
        8-character hex string (e.g., "a1b2c3d4").

    Examples
    --------
    >>> tid = generate_trace_id()
    >>> emit_hook(HookEvent.DOWNLOAD_START, "BTCUSDT", trace_id=tid)
    >>> # ... later ...
    >>> emit_hook(HookEvent.DOWNLOAD_COMPLETE, "BTCUSDT", trace_id=tid)
    """
    return uuid.uuid4().hex[:8]


__all__ = [
    "HookEvent",
    "HookPayload",
    "clear_hooks",
    "emit_hook",
    "generate_trace_id",
    "register_for_all",
    "register_for_failures",
    "register_hook",
    "unregister_hook",
]
