import { WebSocketServer, WebSocket } from 'ws';
import { v4 as uuidv4 } from 'uuid';

console.log("BeMyEars Signaling Server Starting...");

// Override console.log and console.error to include timestamps
const originalLog = console.log;
const originalError = console.error;

function getTimestamp() {
    const now = new Date();
    const dateComponent = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
    const timeComponent = now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true });
    return `${dateComponent} ${timeComponent}`;
}

console.log = (...args: any[]) => {
    originalLog(`[${getTimestamp()}]`, ...args);
};

console.error = (...args: any[]) => {
    originalError(`[${getTimestamp()}]`, ...args);
};

const PORT = 8080;
const wss = new WebSocketServer({ port: PORT });

// Types
type Role = 'caller' | 'interpreter';
type PresenceState = 'UNAVAILABLE' | 'AVAILABLE' | 'RINGING' | 'IN_CALL';

interface User {
    username: string;
    role: Role;
    ws: WebSocket;
    state?: PresenceState; // Only for interpreters
    currentCallId?: string;
}

interface SignalingMessage {
    type: string;
    callId?: string;
    from?: string; // Appended by server for security
    to?: string;
    payload?: any;
    // Client sent properties
    username?: string;
    role?: Role;
}

// State
const users = new Map<string, User>();
const calls = new Map<string, { caller: string; interpreter: string; timer?: NodeJS.Timeout }>();

wss.on('connection', (ws, req) => {
    let ip = req.socket.remoteAddress;
    if (ip && ip.startsWith('::ffff:')) {
        ip = ip.substring(7);
    }
    console.log(`New client connected from ${ip}`);
    let currentUser: User | null = null;

    ws.on('message', (message: any) => {
        try {
            const data: SignalingMessage = JSON.parse(message.toString());
            handleMessage(ws, data, currentUser).then(user => {
                if (user) currentUser = user;
            });
        } catch (e) {
            console.error('Failed to parse message:', e);
        }
    });

    ws.on('close', () => {
        if (currentUser) {
            console.log(`User disconnected: ${currentUser.username}`);
            // Handle disconnect logic (hangup active calls, remove from presence)
            handleDisconnect(currentUser);
        }
    });
});

async function handleMessage(ws: WebSocket, msg: SignalingMessage, currentUser: User | null): Promise<User | null> {
    switch (msg.type) {
        case 'SET_PRESENCE':
            if (currentUser) handleSetPresence(currentUser, msg);
            return currentUser;
        case 'REGISTER':
            return handleRegister(ws, msg);
        case 'CALL_REQUEST':
            if (currentUser) handleCallRequest(currentUser, msg);
            return currentUser;
        case 'CALL_ACCEPT':
            if (currentUser) handleCallAccept(currentUser, msg);
            return currentUser;
        case 'CALL_DECLINE':
            if (currentUser) handleCallDecline(currentUser, msg);
            return currentUser;
        case 'OFFER':
        case 'ANSWER':
        case 'ICE':
        case 'HANGUP':
        case 'VIDEO_VISIBLE':
            if (currentUser) handleSignaling(currentUser, msg);
            return currentUser;
        case 'REPORT_ABUSE':
            if (currentUser) handleReportAbuse(currentUser, msg);
            return currentUser;
        default:
            console.warn(`Unknown message type: ${msg.type}`);
            return currentUser;
    }
}

function handleRegister(ws: WebSocket, msg: SignalingMessage): User | null {
    const username = msg.username || msg.payload?.username;
    const role = msg.role || msg.payload?.role;

    if (!username || !role) {
        console.log("Registration failed: Missing username or role in", msg);
        return null;
    }

    // Simple uniqueness check
    if (users.has(username)) {
        ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Username taken' } }));
        return null;
    }

    const newUser: User = {
        username: username,
        role: role as Role,
        ws: ws,
        state: role === 'interpreter' ? 'UNAVAILABLE' : undefined
    };

    users.set(username, newUser);
    console.log(`✅ CLIENT IDENTIFIED: ${newUser.username} (${newUser.role})`);

    ws.send(JSON.stringify({ type: 'REGISTER_SUCCESS', payload: { username: newUser.username } }));

    broadcastPresence();

    // Send initial presence list to caller
    if (newUser.role === 'caller') {
        sendPresenceList(ws);
    }

    return newUser;
}

function handleCallRequest(caller: User, msg: SignalingMessage) {
    if (caller.role !== 'caller') return;
    const interpreterUsername = msg.to;
    if (!interpreterUsername) return;

    const interpreter = users.get(interpreterUsername);
    if (!interpreter || interpreter.role !== 'interpreter' || interpreter.state !== 'AVAILABLE') {
        caller.ws.send(JSON.stringify({ type: 'BUSY', payload: { message: 'Interpreter unavailable' } }));
        return;
    }

    const callId = uuidv4();

    // Update State
    interpreter.state = 'RINGING';
    interpreter.currentCallId = callId;
    caller.currentCallId = callId;

    calls.set(callId, { caller: caller.username, interpreter: interpreter.username });

    console.log(`📞 Call Request (${callId}): ${caller.username} -> ${interpreter.username}`);

    // Broadcast presence update (interpreter is now RINGING, so hidden/busy)
    broadcastPresence();

    // Forward to interpreter
    interpreter.ws.send(JSON.stringify({
        type: 'CALL_REQUEST',
        callId: callId,
        from: caller.username,
        payload: msg.payload
    }));

    // Start 10s Timer
    const timeout = setTimeout(() => {
        handleCallTimeout(callId);
    }, 10000);

    const callData = calls.get(callId);
    if (callData) callData.timer = timeout;
}

function handleCallAccept(interpreter: User, msg: SignalingMessage) {
    if (interpreter.role !== 'interpreter' || !msg.callId) return;

    const call = calls.get(msg.callId);
    if (!call || call.interpreter !== interpreter.username) return; // Invalid call

    if (call.timer) clearTimeout(call.timer);

    interpreter.state = 'IN_CALL';
    broadcastPresence();

    console.log(`✅ Call Connected (${msg.callId}): ${call.caller} <-> ${interpreter.username}`);

    const caller = users.get(call.caller);
    if (caller) {
        // Send TURN credentials here if we had them dynamic, for now relying on client config or static
        caller.ws.send(JSON.stringify({
            type: 'CALL_ACCEPT',
            callId: msg.callId,
            from: interpreter.username,
            payload: {
                turnServer: {
                    urls: "turn:turn.jaredlog.com:3478",
                    username: "user",
                    credential: "password"
                }
            }
        }));
    }
}

function handleCallDecline(interpreter: User, msg: SignalingMessage) {
    if (interpreter.role !== 'interpreter' || !msg.callId) return;
    const call = calls.get(msg.callId);
    if (!call) return;

    if (call.timer) clearTimeout(call.timer);

    // Reset interpreter
    interpreter.state = 'AVAILABLE';
    interpreter.currentCallId = undefined;
    broadcastPresence();

    // Notify caller
    const caller = users.get(call.caller);
    if (caller) {
        caller.ws.send(JSON.stringify({
            type: 'CALL_DECLINE',
            callId: msg.callId,
            from: interpreter.username
        }));
        caller.currentCallId = undefined;
    }

    calls.delete(msg.callId);
    console.log(`❌ Call Declined (${msg.callId}) by ${interpreter.username}`);
}

function handleCallTimeout(callId: string) {
    const call = calls.get(callId);
    if (!call) return;

    const interpreter = users.get(call.interpreter);
    const caller = users.get(call.caller);

    if (interpreter) {
        interpreter.state = 'AVAILABLE';
        interpreter.currentCallId = undefined;
        // Notify interpreter of timeout?
        interpreter.ws.send(JSON.stringify({ type: 'TIMEOUT', callId }));
    }

    if (caller) {
        caller.ws.send(JSON.stringify({ type: 'BUSY', payload: { message: 'No answer' }, callId }));
        caller.currentCallId = undefined;
    }

    calls.delete(callId);
    broadcastPresence();
}

function handleSignaling(user: User, msg: SignalingMessage) {
    if (!msg.callId || !msg.to) return; // Note: HANGUP might not strictly need 'to' if callId maps it, but keeping for consistency

    // Security check: is this user actually in this call?
    const call = calls.get(msg.callId);
    if (!call) return;
    if (call.caller !== user.username && call.interpreter !== user.username) return;

    if (msg.type === 'HANGUP') {
        handleHangup(user, msg.callId);
        return;
    }

    const targetUser = users.get(msg.to);
    if (targetUser) {
        targetUser.ws.send(JSON.stringify({
            type: msg.type,
            callId: msg.callId,
            from: user.username,
            payload: msg.payload
        }));
    }
}

function handleHangup(user: User, callId: string) {
    const call = calls.get(callId);
    if (!call) return;

    if (call.timer) clearTimeout(call.timer);

    const interpreter = users.get(call.interpreter);
    const caller = users.get(call.caller);

    const otherUser = user.username === call.caller ? interpreter : caller;

    if (otherUser) {
        otherUser.ws.send(JSON.stringify({ type: 'HANGUP', callId, from: user.username }));
        otherUser.currentCallId = undefined;
        if (otherUser.role === 'interpreter') {
            otherUser.state = 'AVAILABLE';
        }
    }

    if (user.role === 'interpreter') {
        user.state = 'AVAILABLE';
    }
    user.currentCallId = undefined;

    calls.delete(callId);
    console.log(`❌ Call Disconnected (${callId}) by ${user.username}`);
    broadcastPresence();
}

function handleDisconnect(user: User) {
    users.delete(user.username);

    if (user.currentCallId) {
        handleHangup(user, user.currentCallId);
    }

    broadcastPresence();
}

function broadcastPresence() {
    const availableInterpreters = Array.from(users.values())
        .filter(u => u.role === 'interpreter' && u.state === 'AVAILABLE')
        .map(u => ({ username: u.username }));

    const message = JSON.stringify({
        type: 'PRESENCE_UPDATE',
        payload: availableInterpreters
    });

    // Calculate stats
    const numInterpreters = Array.from(users.values()).filter(u => u.role === 'interpreter').length;
    const numCallers = Array.from(users.values()).filter(u => u.role === 'caller').length;

    const statsMessage = JSON.stringify({
        type: 'STATS_UPDATE',
        payload: {
            interpreters: numInterpreters,
            callers: numCallers
        }
    });

    for (const u of users.values()) {
        if (u.role === 'caller') {
            u.ws.send(message);
        } else if (u.role === 'interpreter') {
            u.ws.send(statsMessage);
        }
    }
}

function sendPresenceList(ws: WebSocket) {
    const availableInterpreters = Array.from(users.values())
        .filter(u => u.role === 'interpreter' && u.state === 'AVAILABLE')
        .map(u => ({ username: u.username }));

    ws.send(JSON.stringify({
        type: 'PRESENCE_UPDATE',
        payload: availableInterpreters
    }));
}

function handleSetPresence(user: User, msg: SignalingMessage) {
    if (user.role !== 'interpreter') return;

    // Only allow switching between AVAILABLE and UNAVAILABLE if not in call
    if (user.state === 'RINGING' || user.state === 'IN_CALL') {
        user.ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Cannot change presence while in call' } }));
        return;
    }

    const requestedState = msg.payload?.state;
    if (requestedState === 'AVAILABLE' || requestedState === 'UNAVAILABLE') {
        user.state = requestedState;
        console.log(`Presence Update: ${user.username} -> ${user.state}`);
        broadcastPresence();

        // Ack
        user.ws.send(JSON.stringify({ type: 'PRESENCE_CHANGED', payload: { state: user.state } }));
    }
}


function handleReportAbuse(user: User, msg: SignalingMessage) {
    if (user.role !== 'interpreter') return;

    const message = msg.payload?.message;
    const offender = msg.payload?.offender;
    const callId = msg.callId;

    if (!message || !callId) {
        console.warn(`Invalid abuse report from ${user.username}: Missing message or callId`);
        return;
    }

    // Limit message length to 100 chars
    const truncatedMessage = message.substring(0, 100);

    console.log(`⚠️ ABUSE REPORT:
    From: ${user.username}
    About: ${offender || 'Unknown'}
    Call ID: ${callId}
    Message: "${truncatedMessage}"`);
}

import os from 'os';

function getLocalIpAddress() {
    const interfaces = os.networkInterfaces();
    for (const name of Object.keys(interfaces)) {
        for (const iface of interfaces[name]!) {
            // Skip over internal (i.e. 127.0.0.1) and non-IPv4 addresses
            if (iface.family === 'IPv4' && !iface.internal) {
                return iface.address;
            }
        }
    }
    return 'localhost';
}

const localIp = getLocalIpAddress();
console.log(`Server listening on port ${PORT}`);
console.log(`Network access: http://${localIp}:${PORT}`);

// Bonjour / mDNS
import { Bonjour } from 'bonjour-service';
const bonjour = new Bonjour();

bonjour.publish({ name: 'BeMyEars Server', type: 'bemyears', port: PORT, protocol: 'tcp' });
console.log('📢 Advertising Bonjour service: _bemyears._tcp');


// Log user stats every 5 minutes
setInterval(() => {
    const numInterpreters = Array.from(users.values()).filter(u => u.role === 'interpreter').length;
    const numCallers = Array.from(users.values()).filter(u => u.role === 'caller').length;

    console.log(`📊 STATS: ${numInterpreters} Interpreters, ${numCallers} Callers online`);
}, 300000);
