# TRO-K05: Obfuscate event handler names and callbacks
**Area:** Widget.js Telnyx Obfuscation
**Model:** moonshotai/kimi-k2.5
**Tokens:** 8,364
**Cost:** $0.0089
**Time:** 1746ms
**Timestamp:** 2026-02-16T12:40:24.826432

---

I'll provide a complete, production-ready refactoring that abstracts Telnyx-specific events behind a generic voice interface. This includes the client-side widget adapter, backend event handling, and comprehensive tests.

## 1. Event Type Definitions & Constants

```typescript
// widget/src/types/voice-events.ts

/**
 * Generic voice event namespace constants.
 * These abstract the underlying Telnyx SDK implementation.
 */
export const VOICE_EVENT_NAMESPACE = 'voice' as const;
export const TELNYX_EVENT_PREFIX = 'telnyx' as const;

/**
 * Generic voice event types - implementation agnostic.
 */
export enum VoiceEventType {
  READY = 'voice.ready',
  ERROR = 'voice.error',
  NOTIFICATION = 'voice.notification',
  CONNECT = 'voice.connect',
  DISCONNECT = 'voice.disconnect',
  RECONNECTING = 'voice.reconnecting',
  RECONNECTED = 'voice.reconnected',
  
  // Call lifecycle
  CALL_INCOMING = 'voice.call.incoming',
  CALL_ACTIVE = 'voice.call.active',
  CALL_ENDED = 'voice.call.ended',
  CALL_HELD = 'voice.call.held',
  CALL_RESUMED = 'voice.call.resumed',
  
  // Media
  MEDIA_TRACK_ADDED = 'voice.media.track.added',
  MEDIA_TRACK_REMOVED = 'voice.media.track.removed',
  MEDIA_ERROR = 'voice.media.error',
  
  // Signaling
  SIGNALING_STATE_CHANGE = 'voice.signaling.state.change'
}

/**
 * Legacy Telnyx event types (internal use only).
 */
export enum TelnyxEventType {
  READY = 'telnyx.ready',
  ERROR = 'telnyx.error',
  NOTIFICATION = 'telnyx.notification',
  CONNECT = 'telnyx.connect',
  DISCONNECT = 'telnyx.disconnect',
  RECONNECTING = 'telnyx.reconnecting',
  RECONNECTED = 'telnyx.reconnected',
  CALL_INCOMING = 'telnyx.call.incoming',
  CALL_ACTIVE = 'telnyx.call.active',
  CALL_HANGUP = 'telnyx.call.hangup',
  CALL_HELD = 'telnyx.call.held',
  CALL_RESUMED = 'telnyx.call.resumed',
  MEDIA = 'telnyx.media'
}

/**
 * Event mapping configuration.
 */
export const TELNYX_TO_VOICE_EVENT_MAP: Record<string, VoiceEventType> = {
  [TelnyxEventType.READY]: VoiceEventType.READY,
  [TelnyxEventType.ERROR]: VoiceEventType.ERROR,
  [TelnyxEventType.NOTIFICATION]: VoiceEventType.NOTIFICATION,
  [TelnyxEventType.CONNECT]: VoiceEventType.CONNECT,
  [TelnyxEventType.DISCONNECT]: VoiceEventType.DISCONNECT,
  [TelnyxEventType.RECONNECTING]: VoiceEventType.RECONNECTING,
  [TelnyxEventType.RECONNECTED]: VoiceEventType.RECONNECTED,
  [TelnyxEventType.CALL_INCOMING]: VoiceEventType.CALL_INCOMING,
  [TelnyxEventType.CALL_ACTIVE]: VoiceEventType.CALL_ACTIVE,
  [TelnyxEventType.CALL_HANGUP]: VoiceEventType.CALL_ENDED,
  [TelnyxEventType.CALL_HELD]: VoiceEventType.CALL_HELD,
  [TelnyxEventType.CALL_RESUMED]: VoiceEventType.CALL_RESUMED,
  [TelnyxEventType.MEDIA]: VoiceEventType.MEDIA_TRACK_ADDED
};

/**
 * Voice event payload interfaces.
 */
export interface VoiceEventPayload {
  timestamp: number;
  sessionId: string;
  [key: string]: unknown;
}

export interface VoiceErrorPayload extends VoiceEventPayload {
  error: Error;
  code?: string;
  fatal: boolean;
}

export interface CallEventPayload extends VoiceEventPayload {
  callId: string;
  callerNumber?: string;
  callerName?: string;
  direction: 'inbound' | 'outbound';
}

export interface MediaEventPayload extends VoiceEventPayload {
  track: MediaStreamTrack;
  stream: MediaStream;
  kind: 'audio' | 'video';
}
```

## 2. Event Adapter Implementation

```typescript
// widget/src/core/event-adapter.ts

import {
  VoiceEventType,
  TelnyxEventType,
  TELNYX_TO_VOICE_EVENT_MAP,
  VoiceEventPayload,
  VoiceErrorPayload
} from '../types/voice-events';

/**
 * Adapter class that obfuscates Telnyx-specific events behind generic voice events.
 * Implements the Adapter pattern to decouple the widget from Telnyx SDK specifics.
 */
export class VoiceEventAdapter {
  private readonly targetElement: EventTarget;
  private readonly internalHandlers: Map<string, EventListener[]>;
  private readonly isDebugMode: boolean;

  constructor(target: EventTarget, options: { debug?: boolean } = {}) {
    this.targetElement = target;
    this.internalHandlers = new Map();
    this.isDebugMode = options.debug || false;
  }

  /**
   * Registers a Telnyx SDK client and wires up internal event listeners
   * that translate Telnyx events to generic voice events.
   */
  public attachToTelnyxClient(client: TelnyxClient): void {
    // Map each Telnyx event to its generic equivalent
    const mappings: Array<[TelnyxEventType, VoiceEventType]> = [
      [TelnyxEventType.READY, VoiceEventType.READY],
      [TelnyxEventType.ERROR, VoiceEventType.ERROR],
      [TelnyxEventType.NOTIFICATION, VoiceEventType.NOTIFICATION],
      [TelnyxEventType.CONNECT, VoiceEventType.CONNECT],
      [TelnyxEventType.DISCONNECT, VoiceEventType.DISCONNECT],
      [TelnyxEventType.RECONNECTING, VoiceEventType.RECONNECTING],
      [TelnyxEventType.RECONNECTED, VoiceEventType.RECONNECTED],
      [TelnyxEventType.CALL_INCOMING, VoiceEventType.CALL_INCOMING],
      [TelnyxEventType.CALL_ACTIVE, VoiceEventType.CALL_ACTIVE],
      [TelnyxEventType.CALL_HANGUP, VoiceEventType.CALL_ENDED],
      [TelnyxEventType.CALL_HELD, VoiceEventType.CALL_HELD],
      [TelnyxEventType.CALL_RESUMED, VoiceEventType.CALL_RESUMED],
      [TelnyxEventType.MEDIA, VoiceEventType.MEDIA_TRACK_ADDED]
    ];

    mappings.forEach(([telnyxEvent, voiceEvent]) => {
      const handler = this.createEventTranslator(telnyxEvent, voiceEvent);
      client.on(telnyxEvent, handler);
      
      // Store reference for cleanup
      if (!this.internalHandlers.has(telnyxEvent)) {
        this.internalHandlers.set(telnyxEvent, []);
      }
      this.internalHandlers.get(telnyxEvent)!.push(handler as EventListener);
    });

    this.log('Attached to Telnyx client', { events: mappings.length });
  }

  /**
   * Creates an event translator function that converts Telnyx events to generic voice events.
   */
  private createEventTranslator(
    telnyxEvent: TelnyxEventType,
    voiceEvent: VoiceEventType
  ): (data: unknown) => void {
    return (originalData: unknown) => {
      try {
        const genericEvent = this.transformEvent(telnyxEvent, voiceEvent, originalData);
        this.targetElement.dispatchEvent(genericEvent);
        
        this.log('Event translated', {
          from: telnyxEvent,
          to: voiceEvent,
          data: this.sanitizeLogData(originalData)
        });
      } catch (error) {
        console.error(`[VoiceEventAdapter] Failed to translate event ${telnyxEvent}:`, error);
        
        // Dispatch error event on failure
        const errorEvent = new CustomEvent(VoiceEventType.ERROR, {
          detail: {
            originalEvent: telnyxEvent,
            targetEvent: voiceEvent,
            error,
            timestamp: Date.now()
          } as VoiceErrorPayload
        });
        this.targetElement.dispatchEvent(errorEvent);
      }
    };
  }

  /**
   * Transforms Telnyx-specific event data to generic voice event format.
   */
  private transformEvent(
    telnyxEvent: TelnyxEventType,
    voiceEvent: VoiceEventType,
    data: unknown
  ): CustomEvent {
    const payload = this.normalizePayload(telnyxEvent, data);
    
    return new CustomEvent(voiceEvent, {
      detail: payload,
      bubbles: true,
      cancelable: true,
      composed: true // For Shadow DOM
    });
  }

  /**
   * Normalizes Telnyx-specific payload structures to generic voice payload.
   */
  private normalizePayload(
    telnyxEvent: TelnyxEventType,
    data: unknown
  ): VoiceEventPayload {
    const basePayload: VoiceEventPayload = {
      timestamp: Date.now(),
      sessionId: this.extractSessionId(data),
      originalEvent: telnyxEvent // Keep for debugging
    };

    switch (telnyxEvent) {
      case TelnyxEventType.ERROR:
        return {
          ...basePayload,
          error: data instanceof Error ? data : new Error(String(data)),
          fatal: this.isFatalError(data),
          code: this.extractErrorCode(data)
        } as VoiceErrorPayload;

      case TelnyxEventType.CALL_INCOMING:
      case TelnyxEventType.CALL_ACTIVE:
        return {
          ...basePayload,
          callId: this.extractCallId(data),
          callerNumber: this.extractCallerNumber(data),
          callerName: this.extractCallerName(data),
          direction: telnyxEvent === TelnyxEventType.CALL_INCOMING ? 'inbound' : 'outbound'
        };

      case TelnyxEventType.NOTIFICATION:
        return {
          ...basePayload,
          notification: data,
          level: this.extractNotificationLevel(data)
        };

      default:
        return { ...basePayload, raw: data };
    }
  }

  /**
   * Extracts session ID from Telnyx event data.
   */
  private extractSessionId(data: unknown): string {
    if (data && typeof data === 'object') {
      return (data as { sessionId?: string; sipCallId?: string }).sessionId || 
             (data as { sessionId?: string; sipCallId?: string }).sipCallId || 
             'unknown';
    }
    return 'unknown';
  }

  /**
   * Extracts call ID from Telnyx event data.
   */
  private extractCallId(data: unknown): string {
    if (data && typeof data === 'object') {
      return (data as { callId?: string; call_id?: string; callID?: string }).callId || 
             (data as { callId?: string; call_id?: string; callID?: string }).call_id || 
             (data as { callId?: string; call_id?: string; callID?: string }).callID || 
             crypto.randomUUID();
    }
    return crypto.randomUUID();
  }

  private extractCallerNumber(data: unknown): string | undefined {
    if (data && typeof data === 'object') {
      return (data as { callerNumber?: string; from?: string }).callerNumber || 
             (data as { callerNumber?: string; from?: string }).from;
    }
    return undefined;
  }

  private extractCallerName(data: unknown): string | undefined {
    if (data && typeof data === 'object') {
      return (data as { callerName?: string; caller_id_name?: string }).callerName || 
             (data as { callerName?: string; caller_id_name?: string }).caller_id_name;
    }
    return undefined;
  }

  private extractErrorCode(data: unknown): string | undefined {
    if (data && typeof data === 'object') {
      return (data as { code?: string; errorCode?: string }).code || 
             (data as { code?: string; errorCode?: string }).errorCode;
    }
    return undefined;
  }

  private isFatalError(data: unknown): boolean {
    if (data && typeof data === 'object') {
      const fatal = (data as { fatal?: boolean }).fatal;
      if (typeof fatal === 'boolean') return fatal;
      
      const code = this.extractErrorCode(data);
      return ['AUTH_FAILED', 'NETWORK_ERROR', 'MEDIA_ERROR'].includes(code || '');
    }
    return false;
  }

  private extractNotificationLevel(data: unknown): 'info' | 'warning' | 'error' {
    if (data && typeof data === 'object') {
      const level = (data as { level?: string }).level;
      if (level && ['info', 'warning', 'error'].includes(level)) {
        return level as 'info' | 'warning' | 'error';
      }
    }
    return 'info';
  }

  private sanitizeLogData(data: unknown): unknown {
    if (!data || typeof data !== 'object') return data;
    
    // Remove sensitive fields from logs
    const sensitive = ['token', 'password', 'apiKey', 'api_key', 'credential'];
    const sanitized = { ...data as Record<string, unknown> };
    
    sensitive.forEach(field => {
      if (field in sanitized) sanitized[field] = '[REDACTED]';
    });
    
    return sanitized;
  }

  private log(message: string, meta?: Record<string, unknown>): void {
    if (this.isDebugMode) {
      console.log(`[VoiceEventAdapter] ${message}`, meta || '');
    }
  }

  /**
   * Cleanup method to remove all internal handlers.
   */
  public destroy(): void {
    this.internalHandlers.clear();
    this.log('Destroyed');
  }
}

/**
 * Type definition for Telnyx Client interface (simplified).
 */
interface TelnyxClient {
  on(event: string, handler: (...args: unknown[]) => void): void;
  off(event: string, handler: (...args: unknown[]) => void): void;
}
```

## 3. Widget Implementation

```typescript
// widget/src/sunaiva-voice-widget.ts

import { VoiceEventAdapter } from './core/event-adapter';
import { VoiceEventType, VoiceErrorPayload, CallEventPayload } from './types/voice-events';

/**
 * Configuration interface for Sunaiva Voice Widget.
 */
export interface WidgetConfig {
  containerId?: string;
  token: string;
  backendUrl: string;
  debug?: boolean;
  iceServers?: RTCIceServer[];
  audioConstraints?: MediaTrackConstraints;
}

/**
 * Main Sunaiva Voice Widget class.
 * Uses VoiceEventAdapter to abstract Telnyx SDK events.
 */
export class SunaivaVoiceWidget extends HTMLElement {
  private shadow: ShadowRoot;
  private adapter: VoiceEventAdapter | null = null;
  private telnyxClient: any | null = null;
  private config: WidgetConfig;
  private currentCall: any | null = null;
  private eventListeners: Map<VoiceEventType, Set<EventListener>>;
  private audioContext: AudioContext | null = null;

  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: 'closed' });
    this.eventListeners = new Map();
    this.config = this.parseConfig();
    
    this.initializeShadowDOM();
    this.registerGenericEventHandlers();
  }

  /**
   * Parses configuration from attributes or defaults.
   */
  private parseConfig(): WidgetConfig {
    const getAttr = (name: string): string | null => this.getAttribute(name);
    
    return {
      containerId: getAttr('container-id') || undefined,
      token: getAttr('token') || '',
      backendUrl: getAttr('backend-url') || 'https://api.sunaiva.com',
      debug: getAttr('debug') === 'true',
      iceServers: this.parseIceServers(getAttr('ice-servers')),
      audioConstraints: { echoCancellation: true, noiseSuppression: true }
    };
  }

  private parseIceServers(jsonStr: string | null): RTCIceServer[] | undefined {
    if (!jsonStr) return undefined;
    try {
      return JSON.parse(jsonStr);
    } catch {
      console.warn('Failed to parse ICE servers JSON');
      return undefined;
    }
  }

  /**
   * Initializes the Shadow DOM structure.
   */
  private initializeShadowDOM(): void {
    const style = document.createElement('style');
    style.textContent = `
      :host {
        display: block;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        --primary-color: #0066cc;
        --error-color: #dc3545;
        --success-color: #28a745;
      }
      .widget-container {
        padding: 16px;
        border-radius: 8px;
        background: #ffffff;
        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      }
      .status {
        padding: 8px 12px;
        border-radius: 4px;
        margin-bottom: 12px;
        font-size: 14px;
        display: none;
      }
      .status.active { display: block; }
      .status.connecting { background: #fff3cd; color: #856404; }
      .status.ready { background: #d4edda; color: #155724; }
      .status.error { background: #f8d7da; color: #721c24; }
      .status.on-call { background: #cce5ff; color: #004085; }
    `;

    const container = document.createElement('div');
    container.className = 'widget-container';
    container.innerHTML = `
      <div class="status connecting" data-status="connecting">Connecting...</div>
      <div class="status ready" data-status="ready">Ready</div>
      <div class="status error" data-status="error">Error</div>
      <div class="status on-call" data-status="on-call">On Call</div>
      <div class="controls">
        <button id="connect-btn" disabled>Initialize Voice</button>
        <button id="hangup-btn" disabled style="display:none;">Hang Up</button>
      </div>
    `;

    this.shadow.appendChild(style);
    this.shadow.appendChild(container);

    // Wire up button handlers
    const connectBtn = this.shadow.getElementById('connect-btn');
    const hangupBtn = this.shadow.getElementById('hangup-btn');
    
    connectBtn?.addEventListener('click', () => this.initializeVoice());
    hangupBtn?.addEventListener('click', () => this.hangup());
  }

  /**
   * Registers handlers for generic voice events.
   */
  private registerGenericEventHandlers(): void {
    // Use generic event names - no Telnyx references
    this.addEventListener(VoiceEventType.READY, this.handleVoiceReady.bind(this));
    this.addEventListener(VoiceEventType.ERROR, this.handleVoiceError.bind(this));
    this.addEventListener(VoiceEventType.CONNECT, this.handleVoiceConnect.bind(this));
    this.addEventListener(VoiceEventType.DISCONNECT, this.handleVoiceDisconnect.bind(this));
    this.addEventListener(VoiceEventType.CALL_INCOMING, this.handleIncomingCall.bind(this));
    this.addEventListener(VoiceEventType.CALL_ACTIVE, this.handleCallActive.bind(this));
    this.addEventListener(VoiceEventType.CALL_ENDED, this.handleCallEnded.bind(this));
    this.addEventListener(VoiceEventType.NOTIFICATION, this.handleNotification.bind(this));
  }

  /**
   * Initializes the voice connection using Telnyx SDK (internal implementation detail).
   */
  private async initializeVoice(): Promise<void> {
    try {
      // Dynamically import Telnyx SDK to keep it an implementation detail
      const Telnyx = await import('@telnyx/webrtc');
      
      this.telnyxClient = new Telnyx.Client(this.config.token, {
        iceServers: this.config.iceServers,
        ringtoneFile: null, // Use default
        ringbackFile: null
      });

      // Initialize adapter to obfuscate Telnyx events
      this.adapter = new VoiceEventAdapter(this, { debug: this.config.debug });
      this.adapter.attachToTelnyxClient(this.telnyxClient);

      // Connect to Telnyx
      this.telnyxClient.connect();
      
      this.updateStatus('connecting');
      
    } catch (error) {
      console.error('Failed to initialize voice:', error);
      this.dispatchEvent(new CustomEvent(VoiceEventType.ERROR, {
        detail: { error, fatal: true, timestamp: Date.now() }
      }));
    }
  }

  /**
   * Handles voice.ready event.
   */
  private handleVoiceReady(event: Event): void {
    console.log('[SunaivaWidget] Voice system ready');
    this.updateStatus('ready');
    this.updateButtonState('connect', false);
    this.updateButtonState('hangup', false);
    
    // Notify backend of ready state
    this.notifyBackend('voice_ready', (event as CustomEvent).detail);
  }

  /**
   * Handles voice.error event.
   */
  private handleVoiceError(event: Event): void {
    const detail = (event as CustomEvent).detail as VoiceErrorPayload;
    console.error('[SunaivaWidget] Voice error:', detail);
    
    this.updateStatus('error');
    this.updateButtonState('connect', true);
    this.updateButtonState('hangup', false);
    
    // Report to backend error tracking
    this.notifyBackend('voice_error', {
      error: detail.error.message,
      code: detail.code,
      fatal: detail.fatal
    });
  }

  /**
   * Handles voice.connect event.
   */
  private handleVoiceConnect(): void {
    console.log('[SunaivaWidget] Connected to voice service');
  }

  /**
   * Handles voice.disconnect event.
   */
  private handleVoiceDisconnect(): void {
    console.log('[SunaivaWidget] Disconnected from voice service');
    this.updateStatus('ready');
    this.currentCall = null;
  }

  /**
   * Handles voice.call.incoming event.
   */
  private handleIncomingCall(event: Event): void {
    const detail = (event as CustomEvent).detail as CallEventPayload;
    console.log('[SunaivaWidget] Incoming call from:', detail.callerNumber);
    
    // Auto-answer for tradie widget (configurable)
    if (this.telnyxClient && detail.callId) {
      this.telnyxClient.answerCall(detail.callId);
    }
    
    this.updateStatus('on-call');
    this.updateButtonState('hangup', true);
  }

  /**
   * Handles voice.call.active event.
   */
  private handleCallActive(event: Event): void {
    const detail = (event as CustomEvent).detail as CallEventPayload;
    console.log('[SunaivaWidget] Call active:', detail.callId);
    this.currentCall = detail;
    this.updateStatus('on-call');
  }

  /**
   * Handles voice.call.ended event.
   */
  private handleCallEnded(): void {
    console.log('[SunaivaWidget] Call ended');
    this.currentCall = null;
    this.updateStatus('ready');
    this.updateButtonState('hangup', false);
  }

  /**
   * Handles voice.notification event.
   */
  private handleNotification(event: Event): void {
    const detail = (event as CustomEvent).detail;
    console.log('[SunaivaWidget] Notification:', detail);
  }

  /**
   * Hangs up the current call.
   */
  private hangup(): void {
    if (this.telnyxClient && this.currentCall) {
      this.telnyxClient.hangup();
    }
  }

  /**
   * Updates the visual status display.
   */
  private updateStatus(status: string): void {
    const statuses = this.shadow.querySelectorAll('.status');
    statuses.forEach(el => {
      const elStatus = el.getAttribute('data-status');
      el.classList.toggle('active', elStatus === status);
    });
  }

  /**
   * Updates button disabled state.
   */
  private updateButtonState(button: 'connect' | 'hangup', enabled: boolean): void {
    const id = button === 'connect' ? 'connect-btn' : 'hangup-btn';
    const btn = this.shadow.getElementById(id) as HTMLButtonElement | null;
    if (btn) {
      btn.disabled = !enabled;
      btn.style.display = button === 'hangup' && !this.currentCall ? 'none' : 'inline-block';
    }
  }

  /**
   * Notifies backend of voice events.
   */
  private async notifyBackend(eventType: string, payload: unknown): Promise<void> {
    try {
      await fetch(`${this.config.backendUrl}/voice/events`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          event: eventType,
          payload,
          timestamp: new Date().toISOString(),
          widget_id: this.getAttribute('id') || 'unknown'
        })
      });
    } catch (error) {
      console.error('Failed to notify backend:', error);
    }
  }

  /**
   * Public API: Add event listener for generic voice events.
   */
  public onVoiceEvent(
    event: VoiceEventType, 
    handler: (event: CustomEvent) => void
  ): () => void {
    this.addEventListener(event, handler as EventListener);
    return () => this.removeEventListener(event, handler as EventListener);
  }

  /**
   * Cleanup when element is removed from DOM.
   */
  public disconnectedCallback(): void {
    if (this.adapter) {
      this.adapter.destroy();
    }
    if (this.telnyxClient) {
      this.telnyxClient.disconnect();
    }
    if (this.audioContext) {
      this.audioContext.close();
    }
  }
}

// Register custom element
customElements.define('sunaiva-voice-widget', SunaivaVoiceWidget);
```

## 4. Backend Event Handling (Python FastAPI)

```python
# backend/api/voice_events.py

from datetime import datetime
from enum import Enum
from typing import Any, Dict, Optional
from fastapi import APIRouter, HTTPException, Request, BackgroundTasks
from pydantic import BaseModel, Field, validator
import structlog
from redis.asyncio import Redis
from qdrant_client import AsyncQdrantClient

from backend.core.config import Settings
from backend.core.exceptions import VoiceEventError

router = APIRouter(prefix="/voice", tags=["voice"])
logger = structlog.get_logger()
settings = Settings()

class VoiceEventType(str, Enum):
    """Generic voice event types (obfuscated from Telnyx specifics)."""
    READY = "voice.ready"
    ERROR = "voice.error"
    NOTIFICATION = "voice.notification"
    CONNECT = "voice.connect"
    DISCONNECT = "voice.disconnect"
    RECONNECTING = "voice.reconnecting"
    RECONNECTED = "voice.reconnected"
    CALL_INCOMING = "voice.call.incoming"
    CALL_ACTIVE = "voice.call.active"
    CALL_ENDED = "voice.call.ended"
    CALL_HELD = "voice.call.held"
    CALL_RESUMED = "voice.call.resumed"
    MEDIA_TRACK_ADDED = "voice.media.track.added"
    MEDIA_ERROR = "voice.media.error"


class VoiceEventPayload(BaseModel):
    """Base model for voice event payloads."""
    timestamp: int = Field(..., description="Unix timestamp of event")
    session_id: str = Field(..., alias="sessionId", description="Voice session identifier")
    metadata: Optional[Dict[str, Any]] = Field(default=None)
    
    class Config:
        allow_population_by_field_name = True


class VoiceErrorPayload(VoiceEventPayload):
    """Payload for voice.error events."""
    error: str = Field(..., description="Error message")
    code: Optional[str] = Field(None, description="Error code")
    fatal: bool = Field(False, description="Whether error is fatal")
    stack_trace: Optional[str] = Field(None, alias="stackTrace")


class CallEventPayload(VoiceEventPayload):
    """Payload for call-related events."""
    call_id: str = Field(..., alias="callId")
    caller_number: Optional[str] = Field(None, alias="callerNumber")
    caller_name: Optional[str] = Field(None, alias="callerName")
    direction: str = Field(..., pattern="^(inbound|outbound)$")
    duration: Optional[int] = Field(None, description="Call duration in seconds")


class VoiceEventRequest(BaseModel):
    """Request model for voice event reporting."""
    event: VoiceEventType = Field(..., description="Type of voice event")
    payload: Dict[str, Any] = Field(..., description="Event payload data")
    widget_id: str = Field(..., description="Source widget identifier")
    timestamp: datetime = Field(default_factory=datetime.utcnow)
    
    @validator('payload')
    def validate_payload_by_event_type(cls, v: Dict[str, Any], values: Dict[str, Any]) -> Dict[str, Any]:
        """Validates payload structure based on event type."""
        event_type = values.get('event')
        
        if event_type == VoiceEventType.ERROR:
            # Ensure required error fields exist
            if 'error' not in v:
                raise ValueError("Error events must include 'error' field in payload")
                
        elif event_type in [VoiceEventType.CALL_INCOMING, VoiceEventType.CALL_ACTIVE, VoiceEventType.CALL_ENDED]:
            # Ensure call events have call_id
            if 'callId' not in v and 'call_id' not in v:
                raise ValueError("Call events must include 'callId' field")
                
        return v


class VoiceEventHandler:
    """
    Handles processing and storage of generic voice events.
    Obfuscates provider-specific (Telnyx) details from business logic.
    """
    
    def __init__(
        self,
        redis: Redis,
        qdrant: AsyncQdrantClient,
        settings: Settings
    ):
        self.redis = redis
        self.qdrant = qdrant
        self.settings = settings
        self._event_processors = {
            VoiceEventType.ERROR: self._handle_error_event,
            VoiceEventType.CALL_INCOMING: self._handle_incoming_call,
            VoiceEventType.CALL_ENDED: self._handle_call_ended,
            VoiceEventType.READY: self._handle_ready_event,
        }
    
    async def process_event(self, event: VoiceEventRequest) -> Dict[str, Any]:
        """
        Process a generic voice event.
        
        Args:
            event: The voice event to process
            
        Returns:
            Processing result metadata
            
        Raises:
            VoiceEventError: If event processing fails
        """
        try:
            processor = self._event_processors.get(event.event, self._handle_generic_event)
            result = await processor(event)
            
            await self._persist_event(event)
            await self._update_metrics(event)
            
            logger.info(
                "voice_event_processed",
                event_type=event.event.value,
                widget_id=event.widget_id,
                session_id=event.payload.get("sessionId") or event.payload.get("session_id"),
                processing_result=result
            )
            
            return {"status": "processed", "event_id": result.get("id")}
            
        except Exception as e:
            logger.error(
                "voice_event_processing_failed",
                event_type=event.event.value,
                widget_id=event.widget_id,
                error=str(e),
                exc_info=True
            )
            raise VoiceEventError(f"Failed to process voice event: {str(e)}") from e
    
    async def _handle_error_event(self, event: VoiceEventRequest) -> Dict[str, Any]:
        """Handle voice.error events."""
        payload = VoiceErrorPayload(**event.payload)
        
        # Alert on fatal errors
        if payload.fatal:
            await self._alert_ops_team(event, payload)
        
        # Store error in vector DB for pattern analysis
        await self._store_error_vector(event, payload)
        
        return {"id": f"err_{payload.timestamp}", "severity": "high" if payload.fatal else "medium"}
    
    async def _handle_incoming_call(self, event: VoiceEventRequest) -> Dict[str, Any]:
        """Handle voice.call.incoming events."""
        payload = CallEventPayload(**event.payload)
        
        # Cache active call in Redis
        await self.redis.setex(
            f"call:{payload.call_id}",
            3600,  # 1 hour TTL
            payload.json()
        )
        
        # Update widget status
        await self.redis.set(
            f"widget:{event.widget_id}:status",
            "on_call"
        )
        
        return {"id": payload.call_id, "action": "cached"}
    
    async def _handle_call_ended(self, event: VoiceEventRequest) -> Dict[str, Any]:
        """Handle voice.call.ended events."""
        call_id = event.payload.get("callId") or event.payload.get("call_id")
        
        # Remove from active calls
        await self.redis.delete(f"call:{call_id}")
        await self.redis.set(f"widget:{event.widget_id}:status", "ready")
        
        return {"id": call_id, "action": "cleaned"}
    
    async def _handle_ready_event(self, event: VoiceEventRequest) -> Dict[str, Any]:
        """Handle voice.ready events."""
        await self.redis.set(f"widget:{event.widget_id}:status", "ready")
        await self.redis.set(f"widget:{event.widget_id}:last_ready", str(datetime.utcnow().timestamp()))
        return {"id": event.widget_id, "action": "marked_ready"}
    
    async def _handle_generic_event(self, event: VoiceEventRequest) -> Dict[str, Any]:
        """Default handler for unmapped events."""
        return {"id": f"evt_{event.timestamp.timestamp()}", "action": "logged"}
    
    async def _persist_event(self, event: VoiceEventRequest) -> None:
        """Persist event to database."""
        # PostgreSQL persistence would go here
        pass
    
    async def _update_metrics(self, event: VoiceEventRequest) -> None:
        """Update Prometheus/Redis metrics."""
        metric_key = f"metrics:voice:{event.event.value.replace('.', '_')}"
        await self.redis.incr(metric_key)
        await self.redis.expire(metric_key, 86400)  # 24h retention
    
    async def _alert_ops_team(self, event: VoiceEventRequest, payload: VoiceErrorPayload) -> None:
        """Send alert for fatal voice errors."""
        # Integration with PagerDuty/Opsgenie would go here
        logger.error(
            "fatal_voice_error",
            widget_id=event.widget_id,
            error=payload.error,
            code=payload.code,
            requires_attention=True
        )
    
    async def _store_error_vector(self, event: VoiceEventRequest, payload: VoiceErrorPayload) -> None:
        """Store error embedding in Qdrant for similarity search."""
        # Vectorization and storage logic would go here
        pass


# Dependency injection
async def get_event_handler(
    redis: Redis = Depends(get_redis),
    qdrant: AsyncQdrantClient = Depends(get_qdrant)
) -> VoiceEventHandler:
    return VoiceEventHandler(redis, qdrant, settings)


@router.post("/events", response_model=Dict[str, Any])
async def report_voice_event(
    request: VoiceEventRequest,
    background_tasks: BackgroundTasks,
    handler