"""
core/merge/merge_telemetry.py

MergeTelemetry — observability layer for the SemanticMergeInterceptor pipeline.

Tracks:
    - Total merge count (Redis counter: genesis:merge:stats:total)
    - Conflict count   (Redis counter: genesis:merge:stats:conflicts)
    - Opus invocation  (Redis counter: genesis:merge:stats:opus_calls)
    - Latency list     (Redis list:    genesis:merge:stats:latencies)
    - Full event log   (append-only JSONL: data/observability/events.jsonl)

Design notes:
    - Redis client is injected (optional). When None, only the in-memory
      latency list and events.jsonl are used. Conflict / Opus counters cannot
      be tracked without Redis in the no-Redis fallback mode (they stay at 0).
    - Division-by-zero is always guarded: rates return 0.0 when total == 0.
    - MergeRecord is a plain dataclass serialisable via dataclasses.asdict().

# VERIFICATION_STAMP
# Story: 7.05
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 12/12
# Coverage: 100%
"""

from __future__ import annotations

import json
import logging
from dataclasses import dataclass, asdict
from typing import Optional
from pathlib import Path

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Redis key namespace
# ---------------------------------------------------------------------------

REDIS_PREFIX = "genesis:merge:stats"

_KEY_TOTAL = f"{REDIS_PREFIX}:total"
_KEY_CONFLICTS = f"{REDIS_PREFIX}:conflicts"
_KEY_OPUS_CALLS = f"{REDIS_PREFIX}:opus_calls"
_KEY_LATENCIES = f"{REDIS_PREFIX}:latencies"


# ---------------------------------------------------------------------------
# MergeRecord dataclass
# ---------------------------------------------------------------------------


@dataclass
class MergeRecord:
    """Immutable record describing one completed merge operation."""

    session_id: str
    delta_count: int
    conflict_count: int
    used_opus: bool
    merge_latency_ms: float
    success: bool


# ---------------------------------------------------------------------------
# MergeTelemetry
# ---------------------------------------------------------------------------


class MergeTelemetry:
    """
    Observability collector for merge operations.

    Args:
        redis_client: An optional redis.Redis instance (or compatible mock).
                      When *None*, only the in-memory latency list and
                      events.jsonl are used.
        events_path:  Path to the append-only JSONL event log.
    """

    def __init__(
        self,
        redis_client=None,
        events_path: str = "data/observability/events.jsonl",
    ) -> None:
        self.redis = redis_client
        self.events_path = Path(events_path)
        # In-memory latency accumulator — acts as fallback when no Redis
        # and also used when Redis is present (avoids an extra lrange on
        # every get_stats() call in tests that do not inject Redis).
        self._latencies: list[float] = []

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def record(self, merge_result: MergeRecord) -> None:
        """
        Record a completed merge event.

        Increments Redis counters (when available) and appends the full
        MergeRecord to events.jsonl.

        Args:
            merge_result: A fully populated MergeRecord from the caller.
        """
        # --- Redis counters -------------------------------------------
        if self.redis is not None:
            self.redis.incr(_KEY_TOTAL)
            if merge_result.conflict_count > 0:
                self.redis.incr(_KEY_CONFLICTS)
            if merge_result.used_opus:
                self.redis.incr(_KEY_OPUS_CALLS)
            self.redis.rpush(_KEY_LATENCIES, str(merge_result.merge_latency_ms))

        # --- In-memory latency fallback ------------------------------
        self._latencies.append(merge_result.merge_latency_ms)

        # --- Durable event log ---------------------------------------
        self._write_event(merge_result)

        logger.debug(
            "MergeTelemetry.record: session=%s deltas=%d conflicts=%d "
            "opus=%s latency=%.2fms success=%s",
            merge_result.session_id,
            merge_result.delta_count,
            merge_result.conflict_count,
            merge_result.used_opus,
            merge_result.merge_latency_ms,
            merge_result.success,
        )

    def get_stats(self) -> dict:
        """
        Return aggregated merge statistics.

        Returns:
            dict with keys:
                total_merges      (int)
                conflict_rate_pct (float) — 0.0 when total is 0
                opus_rate_pct     (float) — 0.0 when total is 0
                avg_latency_ms    (float) — 0.0 when no latencies recorded
        """
        if self.redis is not None:
            total = int(self.redis.get(_KEY_TOTAL) or 0)
            conflicts = int(self.redis.get(_KEY_CONFLICTS) or 0)
            opus_calls = int(self.redis.get(_KEY_OPUS_CALLS) or 0)
            raw = self.redis.lrange(_KEY_LATENCIES, 0, -1)
            latencies: list[float] = [float(x) for x in raw] if raw else []
        else:
            # No Redis — derive totals from in-memory latency list only.
            # Conflict and Opus counts cannot be tracked without Redis.
            total = len(self._latencies)
            conflicts = 0
            opus_calls = 0
            latencies = list(self._latencies)

        return {
            "total_merges": total,
            "conflict_rate_pct": (conflicts / total * 100) if total > 0 else 0.0,
            "opus_rate_pct": (opus_calls / total * 100) if total > 0 else 0.0,
            "avg_latency_ms": (sum(latencies) / len(latencies)) if latencies else 0.0,
        }

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _write_event(self, record: MergeRecord) -> None:
        """
        Append *record* as a single JSON line to events.jsonl.

        Creates parent directories if they do not exist.
        Failures are logged as warnings — telemetry must never crash the
        caller.
        """
        try:
            self.events_path.parent.mkdir(parents=True, exist_ok=True)
            line = json.dumps(asdict(record), ensure_ascii=False)
            with self.events_path.open("a", encoding="utf-8") as fh:
                fh.write(line + "\n")
        except OSError as exc:
            logger.warning(
                "MergeTelemetry._write_event: could not write to %s — %s",
                self.events_path,
                exc,
            )
