import { Device } from '@twilio/voice-sdk';

let device         = null;
let activeCall     = null;
let incomingCall   = null;   // pending inbound call awaiting accept/reject
let currentToken   = null;
let callTimer      = null;
let callSeconds    = 0;
let tokenRefreshing = false; // guard against concurrent tokenWillExpire events

// Global device state so components that mount after Twilio initialises can
// sync to the current state instead of assuming "offline".
// Values: 'unregistered' | 'registering' | 'registered' | 'error'
window.__twilioDeviceState = 'unregistered';
window.__twilioDeviceError = null;


// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function emitBrowser(name, detail = {}) {
    window.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
    if (window.Livewire) {
        window.Livewire.dispatch(name, detail);
    }
}

async function fetchVoiceToken() {
    const response = await fetch('/twilio/sdk/token', {
        method: 'GET',
        headers: {
            'Accept': 'application/json',
            'X-Requested-With': 'XMLHttpRequest',
        },
        credentials: 'same-origin',
    });

    if (!response.ok) {
        throw new Error(`Failed to fetch Twilio token (${response.status})`);
    }

    const data = await response.json();

    if (!data?.token || typeof data.token !== 'string') {
        throw new Error('Token response did not include a valid token string.');
    }

    return data;
}

function startDurationTimer() {
    stopDurationTimer();
    callSeconds = 0;
    callTimer = window.setInterval(() => {
        callSeconds++;
        // Dispatch a browser-only event (not forwarded to Livewire) so that
        // Alpine.js components can listen without triggering a server round-trip
        // every second.  The CallOverlay and CallManager use their own internal
        // Alpine setInterval timers instead of consuming this event.
        window.dispatchEvent(new CustomEvent('twilio-call-duration-tick', {
            detail:  { seconds: callSeconds },
            bubbles: true,
        }));
    }, 1000);
}

function stopDurationTimer() {
    if (callTimer) {
        clearInterval(callTimer);
        callTimer = null;
    }
}

// ---------------------------------------------------------------------------
// Call event binding  (outbound AND inbound once accepted)
// ---------------------------------------------------------------------------

function bindCallEvents(call) {
    call.on('ringing', () => {
        emitBrowser('twilio-call-ringing');
    });

    call.on('accept', () => {
        activeCall   = call;
        incomingCall = null;

        // The Twilio Voice SDK fires `accept` for both directions, but it
        // means different things:
        //   • Inbound  — the agent accepted the call; it is now connected.
        //   • Outbound — the browser↔Twilio WebRTC media leg is established
        //     for ringback audio; the PSTN recipient has NOT picked up yet
        //     (they may still be ringing or about to decline).
        // For outbound calls we therefore stay silent here and let the
        // server-side `in-progress` status callback (which sets answered_at
        // on the Communication record) drive the transition to connected.
        if (call.direction === 'OUTGOING') {
            return;
        }

        startDurationTimer();
        emitBrowser('twilio-call-accepted');
    });

    call.on('disconnect', () => {
        stopDurationTimer();
        activeCall   = null;
        incomingCall = null;
        emitBrowser('twilio-call-disconnected');
    });

    call.on('cancel', () => {
        stopDurationTimer();
        activeCall   = null;
        incomingCall = null;
        emitBrowser('twilio-call-cancelled');
    });

    call.on('reject', () => {
        stopDurationTimer();
        activeCall   = null;
        incomingCall = null;
        emitBrowser('twilio-call-rejected');
    });

    call.on('error', (error) => {
        stopDurationTimer();
        activeCall   = null;
        incomingCall = null;
        emitBrowser('twilio-call-error', {
            message: error?.message ?? 'Twilio call error.',
            code:    error?.code    ?? null,
        });
    });
}

// ---------------------------------------------------------------------------
// Device initialisation
// ---------------------------------------------------------------------------

export async function initTwilioVoice(debug = false, requestMic = false) {
    // Request microphone permission up-front so the browser doesn't prompt
    // mid-call and so we surface a clear error if access is denied.
    if (requestMic) {
        try {
            await navigator.mediaDevices.getUserMedia({ audio: true });
        } catch {
            throw new Error(
                'Microphone access is required to make calls. ' +
                'Please allow access and reload the page.'
            );
        }
    }

    window.__twilioDeviceState = 'registering';
    emitBrowser('twilio-device-registering');

    const tokenData = await fetchVoiceToken();
    currentToken    = tokenData.token;

    device = new Device(currentToken, {
        logLevel:        debug ? 0 : 1,  // 0 = debug, 1 = info
        codecPreferences: ['opus', 'pcmu'],
        closeProtection: true,
    });

    // ── Lifecycle events ────────────────────────────────────────────────────

    device.on('registered', () => {
        window.__twilioDeviceState = 'registered';
        window.__twilioDeviceError = null;
        emitBrowser('twilio-device-registered');
    });

    device.on('unregistered', () => {
        window.__twilioDeviceState = 'unregistered';
        emitBrowser('twilio-device-unregistered');
    });

    device.on('error', (error) => {
        window.__twilioDeviceState = 'error';
        window.__twilioDeviceError = error?.message ?? 'Twilio device error.';
        emitBrowser('twilio-device-error', {
            message: window.__twilioDeviceError,
            code:    error?.code ?? null,
        });
    });

    // ── Token refresh ────────────────────────────────────────────────────────
    // Fires ~60 s before the access token expires so we can replace it without
    // interrupting any active call.

    device.on('tokenWillExpire', async () => {
        if (tokenRefreshing) return;
        tokenRefreshing = true;
        try {
            const refreshed = await fetchVoiceToken();
            currentToken    = refreshed.token;
            device.updateToken(currentToken);
        } catch (error) {
            console.error('[Twilio] Failed to refresh token:', error);
            emitBrowser('twilio-device-error', {
                message: 'Voice token refresh failed. Please reload the page.',
            });
        } finally {
            tokenRefreshing = false;
        }
    });

    // ── Incoming call ────────────────────────────────────────────────────────
    // Fires when Twilio routes an inbound call to this agent's browser client
    // via <Dial><Client>agent_X</Client></Dial> TwiML.

    device.on('incoming', (call) => {
        incomingCall = call;

        // Bind events now so disconnect/cancel are tracked even before accept.
        bindCallEvents(call);

        emitBrowser('twilio-incoming-call', {
            from:     call.parameters?.From     ?? null,
            call_sid: call.parameters?.CallSid  ?? null,
        });
    });

    await device.register();
}

// ---------------------------------------------------------------------------
// Outbound call
// ---------------------------------------------------------------------------

export async function startBrowserCall(callId) {
    if (!device) {
        throw new Error('Twilio Device is not initialised.');
    }

    emitBrowser('twilio-call-outgoing-starting');

    // device.connect() asks the Twilio TwiML App (identified by the
    // outgoingApplicationSid in the access token) to produce TwiML.
    // The call_id param is forwarded to SdkOutboundVoiceController which
    // looks up the Call record and returns <Dial><Number>…</Number></Dial>.
    const call = await device.connect({
        params: { call_id: String(callId) },
    });

    bindCallEvents(call);

    return call;
}

// ---------------------------------------------------------------------------
// Accept / reject inbound call
// ---------------------------------------------------------------------------

export function acceptIncomingCall() {
    if (incomingCall) {
        incomingCall.accept();
    }
}

export function rejectIncomingCall() {
    if (incomingCall) {
        incomingCall.reject();
        incomingCall = null;
    }
}

// ---------------------------------------------------------------------------
// Hangup / mute
// ---------------------------------------------------------------------------

export function hangupBrowserCall() {
    stopDurationTimer();
    if (activeCall) {
        activeCall.disconnect();
        activeCall = null;
    } else if (device) {
        device.disconnectAll();
    }
}

export function setMute(muted) {
    if (activeCall) {
        activeCall.mute(!!muted);
    }
}

// ---------------------------------------------------------------------------
// Bootstrap  (called once from app.js)
// ---------------------------------------------------------------------------

export function bootstrapTwilioVoice(options = {}) {
    const { debug = false, requestMic = false } = options;

    // Guard: only initialise once even if this function is called multiple
    // times (e.g. Livewire full-page navigations).
    if (window.__twilioBootstrapped) {
        return;
    }
    window.__twilioBootstrapped = true;

    // ── Init on DOM ready ────────────────────────────────────────────────────
    // Vite loads app.js as an ES module which defers execution until after
    // HTML parsing. By the time this code runs DOMContentLoaded may have
    // already fired, so we check readyState first.

    function onReady(fn) {
        if (document.readyState === 'loading') {
            window.addEventListener('DOMContentLoaded', fn, { once: true });
        } else {
            fn();
        }
    }

    onReady(async () => {
        try {
            await initTwilioVoice(debug, requestMic);
        } catch (error) {
            window.__twilioDeviceState = 'error';
            window.__twilioDeviceError = error?.message ?? 'Failed to initialise Twilio.';
            emitBrowser('twilio-device-error', {
                message: window.__twilioDeviceError,
            });
        }
    });

    // ── Outbound call ────────────────────────────────────────────────────────

    window.addEventListener('twilio-start-browser-call', async (event) => {
        try {
            const detail = event.detail;
            const callId = typeof detail === 'object' && detail !== null
                ? detail.call_id ?? detail[0]
                : detail;
            await startBrowserCall(callId);
        } catch (error) {
            emitBrowser('twilio-call-error', {
                message: error?.message ?? 'Failed to start browser call.',
            });
        }
    });

    // ── Inbound call accept / reject ─────────────────────────────────────────

    window.addEventListener('twilio-accept-browser-call', () => {
        acceptIncomingCall();
    });

    window.addEventListener('twilio-reject-browser-call', () => {
        rejectIncomingCall();
    });

    // ── Hangup / mute ────────────────────────────────────────────────────────

    window.addEventListener('twilio-hangup-browser-call', () => {
        hangupBrowserCall();
    });

    window.addEventListener('twilio-mute-browser-call', () => {
        setMute(true);
    });

    window.addEventListener('twilio-unmute-browser-call', () => {
        setMute(false);
    });

    // ── Cleanup on page leave ────────────────────────────────────────────────

    window.addEventListener('beforeunload', () => {
        stopDurationTimer();
        if (device) {
            try { device.destroy(); } catch { /* ignore */ }
        }
    });
}
