
    .i                        U d Z ddlmZ ddlZddlZddlZddlZddlmZ ddl	m	Z	m
Z
 ddlmZ ddlmZmZ  ej                   e      Z ed      Z eh d	      Zd
ed<   e G d d             Z G d d      Zy)us  
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%
    )annotationsN)	dataclass)datetimetimezone)Path)CallableOptionalz9/mnt/e/genesis-system/data/observability/shadow_log.jsonl>   smsemail	crm_writeghl_webhooktelnyx_callexternal_apizfrozenset[str]VALID_EFFECT_TYPESc                  0    e Zd ZU dZded<   ded<   ded<   y)	ShadowResultz5Result of routing a side effect through ShadowRouter.boolexecutedstrmodedict	log_entryN)__name__
__module____qualname____doc____annotations__     3/mnt/e/genesis-system/core/storage/shadow_router.pyr   r   0   s    ?N
IOr   r   c                  @    e Zd ZdZd	dZd
dZddZddZddZddZ	y)ShadowRouteraz  
    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"})
    c                    t         j                  j                  dd      j                         | _        | j                         | _        i | _        y )NSHADOW_MODELIVE)osenvirongetupperdefault_mode_load_overrides
_overrides	_handlers)selfs    r    __init__zShadowRouter.__init__F   s9    !#v!F!L!L!N*.*>*>*@.0r   c                N   t         j                  j                  dd      }	 t        j                  |      }t        |t              si S |j                         D ci c]  \  }}||j                          c}}S c c}}w # t        j                  t        t        f$ r i cY S w xY w)z
        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.
        SHADOW_OVERRIDESz{})r&   r'   r(   jsonloads
isinstancer   itemsr)   JSONDecodeErrorAttributeError	TypeError)r.   raw	overrideskvs        r    r+   zShadowRouter._load_overridesO   s     jjnn/6	

3Ii.	-6__->?TQAqwwyL???$$ni@ 	I	s)   &B 	B A;7B ;B  B$#B$c                <   	 t         j                  j                  dd       t        t         dd      5 }|j	                  t        j                  |      dz          ddd       y# 1 sw Y   yxY w# t        $ r }t        j                  d|       Y d}~yd}~ww xY w)	uD   Append a JSON line to SHADOW_LOG_PATH. Best-effort — never raises.T)parentsexist_okautf-8)encoding
Nz)ShadowRouter: shadow log write failed: %s)
SHADOW_LOG_PATHparentmkdiropenwriter2   dumps	Exceptionloggerwarning)r.   entryfhexcs       r    _write_shadow_logzShadowRouter._write_shadow_log_   s    	M""(((EosW= 3E*T123 3 3 	MNNFLL	Ms4   3A2 (A&A2 &A/+A2 /A2 2	B;BBc                "    || j                   |<   y)z
        Register a real callable for a given effect type.

        The handler will be called as ``handler(payload)`` in LIVE mode.
        N)r-   )r.   effect_typehandlers      r    register_handlerzShadowRouter.register_handlerl   s     '.{#r   c                N    | j                   j                  || j                        S )z
        Return the effective mode for an effect type.

        Per-type overrides (SHADOW_OVERRIDES) take precedence over the
        global SHADOW_MODE default.
        )r,   r(   r*   )r.   rR   s     r    get_modezShadowRouter.get_modet   s!     "";0A0ABBr   c                   |t         vrt        d|dt        t                      | j                  |      }t	        j
                  t        j                  |d      j                  d            j                         }||t        j                  t        j                        j                         |d}|dk(  r| j                  |       t!        dd|	      S | j"                  j%                  |      }|		  ||       t!        dd|	      S # t&        $ r/}t(        j+                  d||       t-        |      |d<   Y d
}~Ad
}~ww xY w)av  
        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.
        zUnknown effect_type: z. Valid types: T)	sort_keysrA   )rR   payload_hash	timestampr   SHADOWF)r   r   r   Nz,ShadowRouter: LIVE handler failed for %s: %serrorr%   )r   
ValueErrorsortedrV   hashlibsha256r2   rI   encode	hexdigestr   nowr   utc	isoformatrP   r   r-   r(   rJ   rK   r\   r   )r.   rR   payloadr   rY   r   rS   rO   s           r    route_side_effectzShadowRouter.route_side_effect}   s=   . 00' 7  &'9 :;= 
 }}[) ~~JJw$/66w?

)+ 	
 '(!hll3==?	
	 8""9-XSS '+nn&8&8&E.  T)LL  .B
 &)X	'".s   7D 	E%E  EN)returnNone)rh   zdict[str, str])rM   r   rh   ri   )rR   r   rS   r   rh   ri   )rR   r   rh   r   )rR   r   rf   r   rh   r   )
r   r   r   r   r/   r+   rP   rT   rV   rg   r   r   r    r"   r"   8   s(    1 M.C<Mr   r"   )r   
__future__r   r_   r2   loggingr&   dataclassesr   r   r   pathlibr   typingr   r	   	getLoggerr   rK   rD   	frozensetr   r   r   r"   r   r   r    <module>rq      s   , #    	 ! '  %			8	$RS%. 0 & N    AM AMr   