# Barge / Whisper / Listen Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Enable supervisors to listen, whisper, and barge into live agent calls from the Helix Engage live monitor, using SIP WebRTC and Ozonetel's admin API.

**Architecture:** Sidecar authenticates to Ozonetel admin API (JWT), proxies barge requests. Frontend uses a separate supervisor SIP client (JsSIP) for audio. DTMF tones (4/5/6) switch between listen/whisper/barge modes. Agent sees a badge on whisper/barge only.

**Tech Stack:** NestJS (sidecar), JsSIP (SIP WebRTC), Ozonetel dashboardApi, React + Recoil atoms, SSE for agent notifications.

**Spec:** `docs/superpowers/specs/2026-04-12-barge-whisper-listen-design.md`

**Prereq:** QA validates barge flow in Ozonetel's own admin UI with the 3 SIP IDs before starting Task 4.

---

## File Map

### New Files (sidecar)
| File | Responsibility |
|------|---------------|
| `helix-engage-server/src/ozonetel/ozonetel-admin-auth.service.ts` | Ozonetel admin JWT lifecycle (login, cache, refresh) |
| `helix-engage-server/src/supervisor/supervisor-barge.controller.ts` | Barge proxy endpoints (initiate, mode, end, SIP credentials) |
| `helix-engage-server/src/supervisor/supervisor-barge.controller.spec.ts` | Unit tests for barge endpoints |

### New Files (frontend)
| File | Responsibility |
|------|---------------|
| `helix-engage/src/lib/supervisor-sip-client.ts` | JsSIP wrapper for supervisor barge sessions |
| `helix-engage/src/components/call-desk/barge-controls.tsx` | Barge connection UI (connect, mode tabs, hangup) |

### Modified Files (sidecar)
| File | Change |
|------|--------|
| `helix-engage-server/src/config/telephony.defaults.ts` | Add `adminUsername`, `adminPassword` to ozonetel config |
| `helix-engage-server/src/supervisor/supervisor.service.ts` | Add barge session tracking + SSE emission for supervisor events |
| `helix-engage-server/src/supervisor/supervisor.module.ts` | Register new controller + auth service |

### Modified Files (frontend)
| File | Change |
|------|--------|
| `helix-engage/src/pages/live-monitor.tsx` | Split layout, context panel, integrate barge controls |
| `helix-engage/src/hooks/use-agent-state.ts` | Handle `supervisor-whisper`, `supervisor-barge`, `supervisor-left` SSE events |
| `helix-engage/src/components/call-desk/active-call-card.tsx` | Supervisor indicator badge |
| `helix-engage/src/components/setup/wizard-step-telephony.tsx` | Admin credential input fields |

---

## Task 1: Extend TelephonyConfig with Admin Credentials

**Files:**
- Modify: `helix-engage-server/src/config/telephony.defaults.ts`
- Modify: `helix-engage-server/src/components/setup/wizard-step-telephony.tsx` (frontend)

- [ ] **Step 1: Add admin fields to TelephonyConfig type**

In `helix-engage-server/src/config/telephony.defaults.ts`, add `adminUsername` and `adminPassword` to the ozonetel section:

```typescript
// In TelephonyConfig type, inside ozonetel:
ozonetel: {
    agentId: string;
    agentPassword: string;
    did: string;
    sipId: string;
    campaignName: string;
    adminUsername: string;  // Ozonetel portal admin login
    adminPassword: string;  // Ozonetel portal admin password
};
```

In `DEFAULT_TELEPHONY_CONFIG`, add empty defaults:

```typescript
ozonetel: {
    agentId: '',
    agentPassword: '',
    did: '',
    sipId: '',
    campaignName: '',
    adminUsername: '',
    adminPassword: '',
},
```

Add to `TELEPHONY_ENV_SEEDS`:

```typescript
{ env: 'OZONETEL_ADMIN_USERNAME', path: ['ozonetel', 'adminUsername'] },
{ env: 'OZONETEL_ADMIN_PASSWORD', path: ['ozonetel', 'adminPassword'] },
```

- [ ] **Step 2: Add admin credential fields to telephony setup wizard**

In `helix-engage/src/components/setup/wizard-step-telephony.tsx`, add two input fields in the Ozonetel section for `adminUsername` and `adminPassword`. Follow the same pattern as the existing `agentId` and `agentPassword` fields. The password field should use `type="password"`.

- [ ] **Step 3: Add admin credentials to masked config**

In `helix-engage-server/src/config/telephony-config.service.ts`, ensure `getMaskedConfig()` masks `ozonetel.adminPassword` the same way it masks `ozonetel.agentPassword`:

```typescript
masked.ozonetel.adminPassword = config.ozonetel.adminPassword ? '***masked***' : '';
```

- [ ] **Step 4: Commit**

```bash
git add helix-engage-server/src/config/telephony.defaults.ts \
       helix-engage-server/src/config/telephony-config.service.ts \
       helix-engage/src/components/setup/wizard-step-telephony.tsx
git commit -m "feat(config): add Ozonetel admin credentials to telephony config"
```

---

## Task 2: Ozonetel Admin Auth Service

**Files:**
- Create: `helix-engage-server/src/ozonetel/ozonetel-admin-auth.service.ts`
- Modify: `helix-engage-server/src/supervisor/supervisor.module.ts`

- [ ] **Step 1: Create the admin auth service**

Create `helix-engage-server/src/ozonetel/ozonetel-admin-auth.service.ts`:

```typescript
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import axios from 'axios';
import { TelephonyConfigService } from '../config/telephony-config.service';

// Ozonetel admin API auth — login with RSA-encrypted credentials, cache JWT.
// Used by supervisor barge endpoints to call dashboardApi.

@Injectable()
export class OzonetelAdminAuthService implements OnModuleInit {
    private readonly logger = new Logger(OzonetelAdminAuthService.name);
    private cachedToken: string | null = null;
    private cachedUserId: string | null = null;
    private cachedUserName: string | null = null;
    private tokenExpiresAt = 0;

    constructor(private readonly telephony: TelephonyConfigService) {}

    async onModuleInit() {
        const config = this.telephony.getConfig();
        if (config.ozonetel.adminUsername && config.ozonetel.adminPassword) {
            this.logger.log('Ozonetel admin credentials configured — will authenticate on first use');
        } else {
            this.logger.warn('Ozonetel admin credentials not configured — supervisor barge will be unavailable');
        }
    }

    private get apiBase(): string {
        return 'https://api.cloudagent.ozonetel.com';
    }

    async getAuthHeaders(): Promise<Record<string, string>> {
        const token = await this.getToken();
        return {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`,
            'userId': this.cachedUserId ?? '',
            'userName': this.cachedUserName ?? '',
            'isSuperAdmin': 'true',
            'dAccessType': 'false',
        };
    }

    async getToken(): Promise<string> {
        if (this.cachedToken && Date.now() < this.tokenExpiresAt) {
            return this.cachedToken;
        }
        return this.login();
    }

    private async login(): Promise<string> {
        const config = this.telephony.getConfig();
        const { adminUsername, adminPassword } = config.ozonetel;

        if (!adminUsername || !adminPassword) {
            throw new Error('Ozonetel admin credentials not configured');
        }

        // Step 1: Get RSA public key
        const preLoginRes = await axios.get(`${this.apiBase}/api/auth/public-key`);
        const { publicKey, keyId } = preLoginRes.data;

        if (!publicKey || !keyId) {
            throw new Error('Failed to get Ozonetel public key');
        }

        // Step 2: RSA-encrypt credentials
        const JSEncrypt = (await import('jsencrypt')).default;
        const encrypt = new JSEncrypt();
        encrypt.setPublicKey(publicKey);

        const encryptedUsername = encrypt.encrypt(adminUsername);
        const encryptedPassword = encrypt.encrypt(adminPassword);

        if (!encryptedUsername || !encryptedPassword) {
            throw new Error('RSA encryption failed');
        }

        // Step 3: Login
        const loginRes = await axios.post(`${this.apiBase}/auth/login`, {
            username: encryptedUsername,
            password: encryptedPassword,
            keyId,
            ltype: 'PORTAL',
        }, {
            headers: { 'Content-Type': 'application/json' },
        });

        const data = loginRes.data;
        if (!data.token) {
            throw new Error(`Ozonetel admin login failed: ${JSON.stringify(data)}`);
        }

        this.cachedToken = data.token;
        this.cachedUserId = data.userId?.toString() ?? data.UserId?.toString() ?? '';
        this.cachedUserName = data.name ?? adminUsername;

        // Decode token expiry — fallback to 6 hours
        try {
            const payload = JSON.parse(Buffer.from(data.token.split('.')[1], 'base64').toString());
            this.tokenExpiresAt = (payload.exp ?? 0) * 1000 - 60_000; // refresh 1 min early
        } catch {
            this.tokenExpiresAt = Date.now() + 6 * 60 * 60 * 1000;
        }

        this.logger.log(`Ozonetel admin login successful (userId=${this.cachedUserId}, expires in ${Math.round((this.tokenExpiresAt - Date.now()) / 60000)}min)`);
        return this.cachedToken;
    }

    isConfigured(): boolean {
        const config = this.telephony.getConfig();
        return !!(config.ozonetel.adminUsername && config.ozonetel.adminPassword);
    }
}
```

- [ ] **Step 2: Register in supervisor module**

In `helix-engage-server/src/supervisor/supervisor.module.ts`, add `OzonetelAdminAuthService` to the providers:

```typescript
import { OzonetelAdminAuthService } from '../ozonetel/ozonetel-admin-auth.service';

@Module({
    // ...existing
    providers: [SupervisorService, OzonetelAdminAuthService],
    exports: [SupervisorService, OzonetelAdminAuthService],
})
```

- [ ] **Step 3: Install jsencrypt**

```bash
cd /Users/satyasumansaridae/Downloads/fortytwo-eap/helix-engage-server
npm install jsencrypt
```

- [ ] **Step 4: Commit**

```bash
git add helix-engage-server/src/ozonetel/ozonetel-admin-auth.service.ts \
       helix-engage-server/src/supervisor/supervisor.module.ts \
       helix-engage-server/package.json helix-engage-server/package-lock.json
git commit -m "feat(sidecar): Ozonetel admin auth service — RSA login, JWT cache, auto-refresh"
```

---

## Task 3: Sidecar Barge Endpoints

**Files:**
- Create: `helix-engage-server/src/supervisor/supervisor-barge.controller.ts`
- Modify: `helix-engage-server/src/supervisor/supervisor.service.ts`
- Modify: `helix-engage-server/src/supervisor/supervisor.module.ts`

- [ ] **Step 1: Add barge session tracking to supervisor service**

In `helix-engage-server/src/supervisor/supervisor.service.ts`, add after the existing properties:

```typescript
// Barge session tracking
type BargeSession = {
    supervisorId: string;
    agentId: string;
    sipNumber: string;
    mode: 'listen' | 'whisper' | 'barge';
    startedAt: string;
};

// Add to class properties:
private readonly bargeSessions = new Map<string, BargeSession>(); // key = agentId

getBargeSession(agentId: string): BargeSession | null {
    return this.bargeSessions.get(agentId) ?? null;
}

startBargeSession(session: BargeSession): void {
    this.bargeSessions.set(session.agentId, session);
    this.logger.log(`[BARGE] ${session.supervisorId} → ${session.agentId} (${session.mode})`);
}

updateBargeMode(agentId: string, mode: 'listen' | 'whisper' | 'barge'): void {
    const session = this.bargeSessions.get(agentId);
    if (!session) return;

    const previousMode = session.mode;
    session.mode = mode;

    // Emit SSE event to agent — only for whisper/barge (listen is silent)
    if (mode === 'whisper' || mode === 'barge') {
        this.agentStateSubject.next({
            agentId,
            state: `supervisor-${mode}` as any,
            timestamp: new Date().toISOString(),
        });
    } else if (previousMode !== 'listen') {
        // Switching back to listen from whisper/barge — tell agent supervisor left
        this.agentStateSubject.next({
            agentId,
            state: 'supervisor-left' as any,
            timestamp: new Date().toISOString(),
        });
    }

    this.logger.log(`[BARGE] Mode: ${agentId} → ${mode}`);
}

endBargeSession(agentId: string): void {
    const session = this.bargeSessions.get(agentId);
    if (!session) return;

    this.bargeSessions.delete(agentId);

    // Notify agent that supervisor left
    this.agentStateSubject.next({
        agentId,
        state: 'supervisor-left' as any,
        timestamp: new Date().toISOString(),
    });

    this.logger.log(`[BARGE] Ended: ${agentId}`);
}
```

- [ ] **Step 2: Create barge controller**

Create `helix-engage-server/src/supervisor/supervisor-barge.controller.ts`:

```typescript
import { Controller, Post, Get, Body, HttpException, Logger } from '@nestjs/common';
import axios from 'axios';
import { OzonetelAdminAuthService } from '../ozonetel/ozonetel-admin-auth.service';
import { SupervisorService } from './supervisor.service';
import { TelephonyConfigService } from '../config/telephony-config.service';

@Controller('api/supervisor/barge')
export class SupervisorBargeController {
    private readonly logger = new Logger(SupervisorBargeController.name);
    private readonly dashboardApiUrl = 'https://api.cloudagent.ozonetel.com/dashboardApi/monitor/api';
    private readonly adminApiUrl = 'https://api.cloudagent.ozonetel.com/ca-admin-Api/CloudAgentAPI';

    constructor(
        private readonly adminAuth: OzonetelAdminAuthService,
        private readonly supervisor: SupervisorService,
        private readonly telephony: TelephonyConfigService,
    ) {}

    @Get('sip-credentials')
    async getSipCredentials() {
        if (!this.adminAuth.isConfigured()) {
            throw new HttpException('Ozonetel admin not configured', 503);
        }

        const config = this.telephony.getConfig();
        const sipGateway = `${config.sip.domain}:${config.sip.wsPort}`;
        const headers = await this.adminAuth.getAuthHeaders();

        try {
            const res = await axios.post(`${this.adminApiUrl}/endpoint/sipnumber/sipSubscribe`, {
                apiId: 139,
                sipURL: sipGateway,
            }, { headers });

            const data = res.data;
            if (!data?.sip_number) {
                throw new HttpException('No SIP numbers available in pool', 503);
            }

            return {
                sipNumber: data.sip_number,
                sipPassword: data.password,
                sipDomain: data.pop_location ?? config.sip.domain,
                sipPort: config.sip.wsPort,
            };
        } catch (err: any) {
            this.logger.error(`SIP credentials failed: ${err.message}`);
            if (err instanceof HttpException) throw err;
            throw new HttpException('Failed to fetch SIP credentials', 502);
        }
    }

    @Post()
    async initiateBarge(@Body() body: { ucid: string; agentId: string; agentNumber: string; supervisorId?: string }) {
        if (!body.ucid || !body.agentNumber) {
            throw new HttpException('ucid and agentNumber required', 400);
        }
        if (!this.adminAuth.isConfigured()) {
            throw new HttpException('Ozonetel admin not configured', 503);
        }

        // Check if already barged into this agent
        const existing = this.supervisor.getBargeSession(body.agentId);
        if (existing) {
            throw new HttpException(`Agent ${body.agentId} already being monitored`, 409);
        }

        // Get SIP credentials first
        const sipCreds = await this.getSipCredentials();
        const headers = await this.adminAuth.getAuthHeaders();

        try {
            const res = await axios.post(this.dashboardApiUrl, {
                apiId: 63,
                ucid: body.ucid,
                action: 'CALL_BARGEIN',
                isSip: true,
                phoneno: sipCreds.sipNumber,
                agentNumber: body.agentNumber,
                cbURL: 'helix-engage',
            }, { headers });

            this.logger.log(`[BARGE] Initiated: ucid=${body.ucid} agent=${body.agentId} sip=${sipCreds.sipNumber} response=${JSON.stringify(res.data)}`);

            // Track the session
            this.supervisor.startBargeSession({
                supervisorId: body.supervisorId ?? 'admin',
                agentId: body.agentId,
                sipNumber: sipCreds.sipNumber,
                mode: 'listen',
                startedAt: new Date().toISOString(),
            });

            return {
                status: 'ok',
                ...sipCreds,
                ozonetelResponse: res.data,
            };
        } catch (err: any) {
            this.logger.error(`[BARGE] Initiation failed: ${err.message}`);
            throw new HttpException(`Barge failed: ${err.response?.data?.Message ?? err.message}`, 502);
        }
    }

    @Post('mode')
    async updateMode(@Body() body: { agentId: string; mode: 'listen' | 'whisper' | 'barge' }) {
        if (!body.agentId || !body.mode) {
            throw new HttpException('agentId and mode required', 400);
        }

        const session = this.supervisor.getBargeSession(body.agentId);
        if (!session) {
            throw new HttpException('No active barge session for this agent', 404);
        }

        this.supervisor.updateBargeMode(body.agentId, body.mode);

        return { status: 'ok', mode: body.mode };
    }

    @Post('end')
    async endBarge(@Body() body: { agentId: string }) {
        if (!body.agentId) {
            throw new HttpException('agentId required', 400);
        }

        const session = this.supervisor.getBargeSession(body.agentId);
        if (!session) {
            return { status: 'ok', message: 'No active session' };
        }

        // Clear Redis tracking on Ozonetel side
        if (this.adminAuth.isConfigured()) {
            try {
                const headers = await this.adminAuth.getAuthHeaders();
                await axios.post(this.dashboardApiUrl, {
                    apiId: 158,
                    Action: 'delete',
                    AgentId: body.agentId,
                    Sip: session.sipNumber,
                }, { headers });
            } catch (err: any) {
                this.logger.warn(`[BARGE] Redis cleanup failed: ${err.message}`);
            }
        }

        this.supervisor.endBargeSession(body.agentId);

        return { status: 'ok' };
    }
}
```

- [ ] **Step 3: Register barge controller in module**

In `helix-engage-server/src/supervisor/supervisor.module.ts`:

```typescript
import { SupervisorBargeController } from './supervisor-barge.controller';

@Module({
    controllers: [SupervisorController, SupervisorBargeController],
    // ...rest stays the same
})
```

- [ ] **Step 4: Commit**

```bash
git add helix-engage-server/src/supervisor/supervisor-barge.controller.ts \
       helix-engage-server/src/supervisor/supervisor.service.ts \
       helix-engage-server/src/supervisor/supervisor.module.ts
git commit -m "feat(sidecar): supervisor barge endpoints — initiate, mode switch, end"
```

---

## Task 4: Supervisor SIP Client (Frontend)

**Files:**
- Create: `helix-engage/src/lib/supervisor-sip-client.ts`

**Prereq:** QA has validated barge works in Ozonetel's own admin UI.

- [ ] **Step 1: Create supervisor SIP client**

Create `helix-engage/src/lib/supervisor-sip-client.ts`:

```typescript
import JsSIP from 'jssip';

type EventCallback = (...args: any[]) => void;
type SupervisorSipEvent =
    | 'registered'
    | 'registrationFailed'
    | 'callReceived'
    | 'callConnected'
    | 'callEnded'
    | 'callFailed';

type SupervisorSipConfig = {
    domain: string;
    port: string;
    number: string;
    password: string;
};

// Lightweight SIP client for supervisor barge sessions.
// Separate from the agent's sip-client.ts — supervisor has different lifecycle.
// Modeled on Ozonetel's kSip utility (CA-Admin/cloudagent.ozonetel.com/static/js/utils/ksip.tsx).

class SupervisorSipClient {
    private ua: JsSIP.UA | null = null;
    private session: JsSIP.RTCSession | null = null;
    private config: SupervisorSipConfig | null = null;
    private listeners = new Map<string, Set<EventCallback>>();
    private audioElement: HTMLAudioElement | null = null;

    init(config: SupervisorSipConfig): void {
        this.config = config;
        this.cleanup();

        // Create hidden audio element for remote audio
        this.audioElement = document.createElement('audio');
        this.audioElement.id = 'supervisor-remote-audio';
        this.audioElement.autoplay = true;
        this.audioElement.setAttribute('playsinline', '');
        document.body.appendChild(this.audioElement);

        const socketUrl = `wss://${config.domain}:${config.port}`;
        const socket = new JsSIP.WebSocketInterface(socketUrl);

        this.ua = new JsSIP.UA({
            sockets: [socket],
            uri: `sip:${config.number}@${config.domain}`,
            password: config.password,
            registrar_server: `sip:${config.domain}`,
            authorization_user: config.number,
            session_timers: false,
            register: false,
        });

        this.ua.on('registered', () => {
            this.emit('registered');
        });

        this.ua.on('registrationFailed', (e: any) => {
            this.emit('registrationFailed', e?.cause);
        });

        this.ua.on('newRTCSession', (data: any) => {
            const rtcSession = data.session as JsSIP.RTCSession;
            if (rtcSession.direction !== 'incoming') return;

            this.session = rtcSession;
            this.emit('callReceived');

            // Auto-answer incoming call from Ozonetel
            rtcSession.on('accepted', () => {
                this.emit('callConnected');
            });

            rtcSession.on('confirmed', () => {
                // Attach remote audio
                const connection = rtcSession.connection;
                if (connection && this.audioElement) {
                    const remoteStreams = connection.getRemoteStreams?.();
                    if (remoteStreams?.[0]) {
                        this.audioElement.srcObject = remoteStreams[0];
                    }
                    // Modern browsers: use track event
                    connection.addEventListener('track', (event: RTCTrackEvent) => {
                        if (event.streams[0] && this.audioElement) {
                            this.audioElement.srcObject = event.streams[0];
                        }
                    });
                }
            });

            rtcSession.on('ended', () => {
                this.session = null;
                this.emit('callEnded');
            });

            rtcSession.on('failed', (e: any) => {
                this.session = null;
                this.emit('callFailed', e?.cause);
            });

            // Auto-answer with audio
            rtcSession.answer({
                mediaConstraints: { audio: true, video: false },
            });
        });

        this.ua.start();
    }

    register(): void {
        this.ua?.register();
    }

    isRegistered(): boolean {
        return this.ua?.isRegistered() ?? false;
    }

    isCallActive(): boolean {
        return this.session?.isEstablished() ?? false;
    }

    sendDTMF(digit: string): void {
        if (!this.session?.isEstablished()) return;
        this.session.sendDTMF(digit, {
            duration: 160,
            interToneGap: 1200,
        });
    }

    hangup(): void {
        if (this.session) {
            try {
                this.session.terminate();
            } catch {
                // Session may already be ended
            }
            this.session = null;
        }
    }

    close(): void {
        this.hangup();
        if (this.ua) {
            this.ua.unregister({ all: true });
            this.ua.stop();
            this.ua = null;
        }
        this.cleanup();
    }

    on(event: SupervisorSipEvent, callback: EventCallback): void {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, new Set());
        }
        this.listeners.get(event)!.add(callback);
    }

    off(event: SupervisorSipEvent, callback: EventCallback): void {
        this.listeners.get(event)?.delete(callback);
    }

    private emit(event: string, ...args: any[]): void {
        this.listeners.get(event)?.forEach(cb => {
            try { cb(...args); } catch (e) { console.error(`SupervisorSIP event error [${event}]:`, e); }
        });
    }

    private cleanup(): void {
        if (this.audioElement) {
            this.audioElement.srcObject = null;
            this.audioElement.remove();
            this.audioElement = null;
        }
    }
}

export const supervisorSip = new SupervisorSipClient();
```

- [ ] **Step 2: Commit**

```bash
git add helix-engage/src/lib/supervisor-sip-client.ts
git commit -m "feat(frontend): supervisor SIP client — JsSIP wrapper for barge sessions"
```

---

## Task 5: Barge Controls Component

**Files:**
- Create: `helix-engage/src/components/call-desk/barge-controls.tsx`

- [ ] **Step 1: Create barge controls component**

Create `helix-engage/src/components/call-desk/barge-controls.tsx`:

```typescript
import { useState, useEffect, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPhone, faPhoneHangup, faHeadset, faCommentDots, faUsers } from '@fortawesome/pro-duotone-svg-icons';
import { faIcon } from '@/lib/icon-wrapper';
import { Button } from '@/components/base/buttons/button';
import { Badge } from '@/components/base/badges/badges';
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
import { supervisorSip } from '@/lib/supervisor-sip-client';
import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast';
import { cx } from '@/utils/cx';

const PhoneIcon = faIcon(faPhone);
const HangupIcon = faIcon(faPhoneHangup);

type BargeStatus = 'idle' | 'connecting' | 'connected' | 'ended';
type BargeMode = 'listen' | 'whisper' | 'barge';

const MODE_DTMF: Record<BargeMode, string> = { listen: '4', whisper: '5', barge: '6' };
const MODE_LABELS: Record<BargeMode, { label: string; description: string; color: 'gray' | 'brand' | 'error' }> = {
    listen: { label: 'Listen', description: 'Silent monitoring — nobody knows you are here', color: 'gray' },
    whisper: { label: 'Whisper', description: 'Only the agent can hear you', color: 'brand' },
    barge: { label: 'Barge', description: 'Both agent and patient can hear you', color: 'error' },
};

type BargeControlsProps = {
    ucid: string;
    agentId: string;
    agentNumber: string;
    agentName: string;
    callerNumber: string;
    onDisconnected?: () => void;
};

export const BargeControls = ({ ucid, agentId, agentNumber, agentName, callerNumber, onDisconnected }: BargeControlsProps) => {
    const [status, setStatus] = useState<BargeStatus>('idle');
    const [mode, setMode] = useState<BargeMode>('listen');
    const [duration, setDuration] = useState(0);
    const connectedAtRef = useRef<number | null>(null);

    // Duration counter
    useEffect(() => {
        if (status !== 'connected') return;
        connectedAtRef.current = Date.now();
        const interval = setInterval(() => {
            setDuration(Math.floor((Date.now() - (connectedAtRef.current ?? Date.now())) / 1000));
        }, 1000);
        return () => clearInterval(interval);
    }, [status]);

    // Cleanup on unmount
    useEffect(() => {
        return () => {
            if (supervisorSip.isCallActive()) {
                supervisorSip.close();
                apiClient.post('/api/supervisor/barge/end', { agentId }, { silent: true }).catch(() => {});
            }
        };
    }, [agentId]);

    const handleConnect = async () => {
        setStatus('connecting');

        try {
            // Call sidecar barge endpoint — gets SIP creds + initiates barge on Ozonetel
            const result = await apiClient.post<{
                sipNumber: string;
                sipPassword: string;
                sipDomain: string;
                sipPort: string;
            }>('/api/supervisor/barge', { ucid, agentId, agentNumber });

            // Initialize supervisor SIP client
            supervisorSip.on('registered', () => {
                // SIP registered — Ozonetel will send incoming call
            });

            supervisorSip.on('callConnected', () => {
                setStatus('connected');
                // Default mode is listen — send DTMF 4
                supervisorSip.sendDTMF('4');
                notify.success('Connected', `Monitoring ${agentName}'s call`);
            });

            supervisorSip.on('callEnded', () => {
                setStatus('ended');
                apiClient.post('/api/supervisor/barge/end', { agentId }, { silent: true }).catch(() => {});
                onDisconnected?.();
            });

            supervisorSip.on('callFailed', (cause: string) => {
                setStatus('ended');
                notify.error('Connection Failed', cause ?? 'Could not connect to call');
                apiClient.post('/api/supervisor/barge/end', { agentId }, { silent: true }).catch(() => {});
            });

            supervisorSip.on('registrationFailed', (cause: string) => {
                setStatus('ended');
                notify.error('SIP Registration Failed', cause ?? 'Could not register SIP');
            });

            supervisorSip.init({
                domain: result.sipDomain,
                port: result.sipPort,
                number: result.sipNumber,
                password: result.sipPassword,
            });
            supervisorSip.register();
        } catch (err: any) {
            setStatus('idle');
            notify.error('Barge Failed', err.message ?? 'Could not initiate barge');
        }
    };

    const handleModeChange = (newMode: BargeMode) => {
        if (newMode === mode) return;
        supervisorSip.sendDTMF(MODE_DTMF[newMode]);
        setMode(newMode);
        apiClient.post('/api/supervisor/barge/mode', { agentId, mode: newMode }, { silent: true }).catch(() => {});
    };

    const handleHangup = () => {
        supervisorSip.close();
        setStatus('ended');
        apiClient.post('/api/supervisor/barge/end', { agentId }, { silent: true }).catch(() => {});
        onDisconnected?.();
    };

    const formatDuration = (sec: number) => {
        const m = Math.floor(sec / 60);
        const s = sec % 60;
        return `${m}:${s.toString().padStart(2, '0')}`;
    };

    if (status === 'idle' || status === 'ended') {
        return (
            <div className="flex flex-col items-center gap-3 py-6">
                <FontAwesomeIcon icon={faHeadset} className="size-8 text-fg-quaternary" />
                <p className="text-sm text-secondary">{status === 'ended' ? 'Session ended' : 'Ready to monitor'}</p>
                <Button
                    size="sm"
                    color="primary"
                    iconLeading={PhoneIcon}
                    onClick={handleConnect}
                >
                    {status === 'ended' ? 'Reconnect' : 'Connect'}
                </Button>
            </div>
        );
    }

    if (status === 'connecting') {
        return (
            <div className="flex flex-col items-center gap-3 py-6">
                <div className="flex items-center gap-2">
                    <span className="size-2 animate-pulse rounded-full bg-warning-solid" />
                    <span className="text-sm font-medium text-warning-primary">Connecting...</span>
                </div>
                <p className="text-xs text-tertiary">Registering SIP and joining call</p>
            </div>
        );
    }

    // Connected
    return (
        <div className="flex flex-col gap-3">
            {/* Status bar */}
            <div className="flex items-center justify-between rounded-lg bg-success-primary px-3 py-2">
                <div className="flex items-center gap-2">
                    <span className="size-2 rounded-full bg-success-solid" />
                    <span className="text-xs font-semibold text-success-primary">Connected</span>
                </div>
                <span className="font-mono text-xs text-success-primary">{formatDuration(duration)}</span>
            </div>

            {/* Mode tabs */}
            <div className="flex gap-1">
                {(['listen', 'whisper', 'barge'] as BargeMode[]).map((m) => {
                    const config = MODE_LABELS[m];
                    const isActive = mode === m;
                    return (
                        <button
                            key={m}
                            onClick={() => handleModeChange(m)}
                            className={cx(
                                'flex flex-1 flex-col items-center gap-1 rounded-lg border-2 px-2 py-2.5 text-center transition duration-100 ease-linear',
                                isActive
                                    ? m === 'listen' ? 'border-secondary bg-secondary' : m === 'whisper' ? 'border-brand bg-brand-primary' : 'border-error bg-error-primary'
                                    : 'border-secondary hover:bg-primary_hover',
                            )}
                        >
                            <FontAwesomeIcon
                                icon={m === 'listen' ? faHeadset : m === 'whisper' ? faCommentDots : faUsers}
                                className={cx('size-4', isActive ? 'text-fg-primary' : 'text-fg-quaternary')}
                            />
                            <span className={cx('text-xs font-semibold', isActive ? 'text-primary' : 'text-tertiary')}>
                                {config.label}
                            </span>
                        </button>
                    );
                })}
            </div>

            {/* Mode description */}
            <p className="text-center text-xs text-tertiary">{MODE_LABELS[mode].description}</p>

            {/* Hang up */}
            <Button size="sm" color="primary-destructive" iconLeading={HangupIcon} onClick={handleHangup} className="w-full">
                Hang Up
            </Button>
        </div>
    );
};
```

- [ ] **Step 2: Commit**

```bash
git add helix-engage/src/components/call-desk/barge-controls.tsx
git commit -m "feat(frontend): barge controls component — connect, mode tabs, hangup"
```

---

## Task 6: Live Monitor Redesign

**Files:**
- Modify: `helix-engage/src/pages/live-monitor.tsx`

- [ ] **Step 1: Redesign live monitor with split layout**

Rewrite `helix-engage/src/pages/live-monitor.tsx` to split into left (call list) and right (context + barge controls) panels. The left panel keeps the existing KPI cards + table. The right panel shows caller context when a row is selected, plus the BargeControls component.

Key changes:
1. Add `selectedCall` state (`ActiveCall | null`)
2. Make table rows clickable — set `selectedCall` on click
3. Right panel: when `selectedCall` is set, fetch caller context from leads by phone match, show patient summary (name, AI summary, source, appointments), render `<BargeControls>` below
4. Replace disabled Listen/Whisper/Barge buttons with a single "Monitor" button that selects the row
5. Keep KPI cards above the split layout

The right panel follows the same pattern as the existing `context-panel.tsx` but simplified — no appointment form, no enquiry form, just read-only context + barge controls.

This is a larger rewrite. Preserve the existing polling logic, KPI cards, caller name resolution, and table structure. Add the split layout wrapper and the right panel.

- [ ] **Step 2: Test manually**

1. Run `npm run dev` in helix-engage
2. Navigate to `/live-monitor` as admin
3. Verify: split layout renders, KPI cards visible, table shows active calls
4. Verify: clicking a row highlights it and right panel shows caller context
5. Verify: BargeControls shows "Ready to monitor" with Connect button

- [ ] **Step 3: Commit**

```bash
git add helix-engage/src/pages/live-monitor.tsx
git commit -m "feat(frontend): live monitor split layout with context panel and barge controls"
```

---

## Task 7: Agent-Side Supervisor Indicator

**Files:**
- Modify: `helix-engage/src/hooks/use-agent-state.ts`
- Modify: `helix-engage/src/components/call-desk/active-call-card.tsx`

- [ ] **Step 1: Handle supervisor SSE events in use-agent-state hook**

In `helix-engage/src/hooks/use-agent-state.ts`, the SSE message handler (around line 37-63) processes `data.state`. Add handling for supervisor states. These don't replace the agent's Ozonetel state — they're a parallel signal.

Add a new exported atom and update the hook:

```typescript
import { atom, useSetRecoilState } from 'recoil';

export const supervisorPresenceAtom = atom<'none' | 'whisper' | 'barge'>({
    key: 'supervisorPresence',
    default: 'none',
});
```

In the SSE message handler, after the existing state handling:

```typescript
// Handle supervisor presence events
if (data.state === 'supervisor-whisper') {
    setSupervisorPresence('whisper');
    return; // Don't update agent state
}
if (data.state === 'supervisor-barge') {
    setSupervisorPresence('barge');
    return;
}
if (data.state === 'supervisor-left') {
    setSupervisorPresence('none');
    return;
}
```

The hook needs `useSetRecoilState(supervisorPresenceAtom)` added.

- [ ] **Step 2: Add supervisor badge to active call card**

In `helix-engage/src/components/call-desk/active-call-card.tsx`, import the atom and render a badge:

```typescript
import { useRecoilValue } from 'recoil';
import { supervisorPresenceAtom } from '@/hooks/use-agent-state';

// Inside the component:
const supervisorPresence = useRecoilValue(supervisorPresenceAtom);
```

Add the badge in the caller info header section (around line 227, after the caller name):

```typescript
{supervisorPresence === 'whisper' && (
    <Badge size="sm" color="blue" type="pill-color">Supervisor coaching</Badge>
)}
{supervisorPresence === 'barge' && (
    <Badge size="sm" color="brand" type="pill-color">Supervisor on call</Badge>
)}
```

- [ ] **Step 3: Test manually**

1. Open two browser tabs — one as admin (supervisor), one as cc-agent
2. Agent is on a call
3. From admin tab: navigate to live monitor, select the call, click Connect
4. Verify: agent sees "Supervisor coaching" badge when supervisor switches to Whisper
5. Verify: agent sees "Supervisor on call" badge when supervisor switches to Barge
6. Verify: badge disappears when supervisor switches back to Listen or hangs up

- [ ] **Step 4: Commit**

```bash
git add helix-engage/src/hooks/use-agent-state.ts \
       helix-engage/src/components/call-desk/active-call-card.tsx
git commit -m "feat(frontend): supervisor presence indicator on agent call card"
```

---

## Task 8: Integration Testing

**Files:** No new files — manual test checklist.

- [ ] **Step 1: Prereq check**

Verify QA has confirmed barge works in Ozonetel's own admin UI with the hospital's account.

- [ ] **Step 2: Configure admin credentials**

Add Ozonetel admin username/password to the telephony config via the setup wizard or directly in `data/telephony.json`:

```json
{
    "ozonetel": {
        "adminUsername": "<ozonetel-admin-username>",
        "adminPassword": "<ozonetel-admin-password>",
        ...
    }
}
```

- [ ] **Step 3: End-to-end barge test**

1. Login as cc-agent → receive or make a call
2. Login as admin in separate browser → go to Live Monitor
3. Click the active call row → verify context panel shows caller info
4. Click "Connect" → verify status changes to "Connecting" then "Connected"
5. Verify: you can hear the call audio (Listen mode)
6. Click "Whisper" tab → verify agent hears you, patient doesn't
7. Click "Barge" tab → verify both agent and patient hear you
8. Click "Listen" tab → verify you go silent again
9. Click "Hang Up" → verify session ends cleanly
10. Verify: agent's badge disappears

- [ ] **Step 4: Auto-disconnect test**

1. While supervisor is barged in, have the agent end the call
2. Verify: supervisor's SIP session auto-disconnects
3. Verify: barge controls show "Session ended"
4. Verify: agent's supervisor badge disappears

- [ ] **Step 5: Edge case tests**

- Supervisor navigates away from live monitor mid-barge → verify cleanup on unmount
- Supervisor tries to barge two agents simultaneously → verify 409 error
- Agent goes to ACW while supervisor is barged → verify auto-disconnect
- Network drop during barge → verify SIP reconnection or clean failure

- [ ] **Step 6: Final commit**

```bash
git add -A
git commit -m "test: verify barge/whisper/listen integration"
```
