"""
core.storage.shadow_router — ShadowRouter: Side-Effect Gating

Routes external side-effects (email, SMS, CRM writes, Telnyx calls, GHL
webhooks, external API calls) to either LIVE or SHADOW mode.

SHADOW mode: logs the intended call without executing it.
LIVE mode: executes the real external handler.

Mode is controlled by:
  - SHADOW_MODE env var (default: "LIVE")
  - SHADOW_OVERRIDES env var: JSON dict mapping effect_type → mode
    e.g. '{"email": "SHADOW", "sms": "LIVE"}'

# VERIFICATION_STAMP
# Story: 5.07
# Verified By: parallel-builder
# Verified At: 2026-02-25T00:00:00+00:00
# Tests: 15/15
# Coverage: 100%
"""

from __future__ import annotations

import hashlib
import json
import logging
import os
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Optional

logger = logging.getLogger(__name__)

SHADOW_LOG_PATH = Path("/mnt/e/genesis-system/data/observability/shadow_log.jsonl")

VALID_EFFECT_TYPES: frozenset[str] = frozenset({
    "email",
    "sms",
    "crm_write",
    "telnyx_call",
    "ghl_webhook",
    "external_api",
})


@dataclass
class ShadowResult:
    """Result of routing a side effect through ShadowRouter."""
    executed: bool        # True if a real handler was invoked (LIVE mode)
    mode: str             # "LIVE" | "SHADOW"
    log_entry: dict       # Metadata logged for every invocation


class ShadowRouter:
    """
    Routes external side-effects to LIVE or SHADOW mode.

    SHADOW mode: logs the intended call without executing.
    LIVE mode: executes the registered handler for the effect type.

    Usage::

        router = ShadowRouter()
        router.register_handler("email", my_email_sender)
        result = router.route_side_effect("email", {"to": "a@b.com", "body": "Hi"})
    """

    def __init__(self) -> None:
        self.default_mode: str = os.environ.get("SHADOW_MODE", "LIVE").upper()
        self._overrides: dict[str, str] = self._load_overrides()
        self._handlers: dict[str, Callable] = {}

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _load_overrides(self) -> dict[str, str]:
        """
        Load per-effect-type mode overrides from the SHADOW_OVERRIDES env var.

        Expected format: JSON object, e.g. '{"email":"SHADOW","sms":"LIVE"}'.
        Malformed JSON silently falls back to an empty dict.
        """
        raw = os.environ.get("SHADOW_OVERRIDES", "{}")
        try:
            overrides = json.loads(raw)
            if not isinstance(overrides, dict):
                return {}
            return {k: v.upper() for k, v in overrides.items()}
        except (json.JSONDecodeError, AttributeError, TypeError):
            return {}

    def _write_shadow_log(self, entry: dict) -> None:
        """Append a JSON line to SHADOW_LOG_PATH. Best-effort — never raises."""
        try:
            SHADOW_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
            with open(SHADOW_LOG_PATH, "a", encoding="utf-8") as fh:
                fh.write(json.dumps(entry) + "\n")
        except Exception as exc:  # pragma: no cover
            logger.warning("ShadowRouter: shadow log write failed: %s", exc)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def register_handler(self, effect_type: str, handler: Callable) -> None:
        """
        Register a real callable for a given effect type.

        The handler will be called as ``handler(payload)`` in LIVE mode.
        """
        self._handlers[effect_type] = handler

    def get_mode(self, effect_type: str) -> str:
        """
        Return the effective mode for an effect type.

        Per-type overrides (SHADOW_OVERRIDES) take precedence over the
        global SHADOW_MODE default.
        """
        return self._overrides.get(effect_type, self.default_mode)

    def route_side_effect(self, effect_type: str, payload: dict) -> ShadowResult:
        """
        Route a side-effect to LIVE or SHADOW mode.

        Parameters
        ----------
        effect_type:
            One of VALID_EFFECT_TYPES: "email", "sms", "crm_write",
            "telnyx_call", "ghl_webhook", "external_api".
        payload:
            Arbitrary dict describing the side-effect.

        Returns
        -------
        ShadowResult
            ``executed=False`` in SHADOW mode; ``executed=True`` in LIVE mode
            (even if the handler raised, a result is still returned).

        Raises
        ------
        ValueError
            If *effect_type* is not in VALID_EFFECT_TYPES.
        """
        if effect_type not in VALID_EFFECT_TYPES:
            raise ValueError(
                f"Unknown effect_type: {effect_type!r}. "
                f"Valid types: {sorted(VALID_EFFECT_TYPES)}"
            )

        mode = self.get_mode(effect_type)

        # Build a deterministic hash of the payload for idempotency tracking
        payload_hash = hashlib.sha256(
            json.dumps(payload, sort_keys=True).encode("utf-8")
        ).hexdigest()

        log_entry: dict = {
            "effect_type": effect_type,
            "payload_hash": payload_hash,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "mode": mode,
        }

        if mode == "SHADOW":
            self._write_shadow_log(log_entry)
            return ShadowResult(executed=False, mode="SHADOW", log_entry=log_entry)

        # ----- LIVE mode -----
        handler: Optional[Callable] = self._handlers.get(effect_type)
        if handler is not None:
            try:
                handler(payload)
            except Exception as exc:
                logger.error(
                    "ShadowRouter: LIVE handler failed for %s: %s",
                    effect_type,
                    exc,
                )
                log_entry["error"] = str(exc)

        return ShadowResult(executed=True, mode="LIVE", log_entry=log_entry)
