import { Server, Socket } from 'socket.io';
import { db } from '../config/firebase';
import { doc, getDoc, updateDoc } from 'firebase/firestore';

interface MeetingMeta {
  hostId: string;
  isActive: boolean;
}

interface PendingRequest {
  viewerId: string;
  name: string;
}

export const setupSocketEvents = (io: Server) => {
  const sessionHosts = new Map<string, string>();
  const meetingCache = new Map<string, MeetingMeta>();
  const pendingRequests = new Map<string, PendingRequest[]>();
  const socketQueue = new Map<string, unknown[]>();

  const getMeetingMeta = async (sessionId: string): Promise<MeetingMeta | null> => {
    if (meetingCache.has(sessionId)) {
      return meetingCache.get(sessionId)!;
    }
    try {
      const snap = await getDoc(doc(db, 'meetings', sessionId));
      if (!snap.exists()) return null;
      const data = snap.data();
      const meta: MeetingMeta = {
        hostId: data.hostId,
        isActive: data.isActive !== false
      };
      meetingCache.set(sessionId, meta);
      return meta;
    } catch {
      return null;
    }
  };

  const flushQueue = (sessionId: string) => {
    const queue = socketQueue.get(sessionId) || [];
    socketQueue.delete(sessionId);
    queue.forEach(fn => { (fn as () => void)(); });
  };

  io.on('connection', (socket: Socket) => {
    console.log('User connected:', socket.id);

    socket.on('join-session', async (data: { sessionId: string; userId?: string } | string) => {
      const sessionId = typeof data === 'string' ? data : data.sessionId;
      const userId = typeof data === 'string' ? null : data.userId;

      if (userId) {
        const meta = await getMeetingMeta(sessionId);
        if (!meta || !meta.isActive) {
          socket.emit('meeting-ended', { sessionId });
          return;
        }
        if (!sessionHosts.has(sessionId)) {
          sessionHosts.set(sessionId, meta.hostId);
        }
      }

      socket.join(sessionId);
      socket.data.sessionId = sessionId;
      if (userId) {
        socket.data.userId = userId;
      }

      const participants = Array.from(io.sockets.adapter.rooms.get(sessionId) || [])
        .filter(id => id !== socket.id)
        .map(id => {
          const s = io.sockets.sockets.get(id);
          return {
            viewerId: id,
            name: (s?.data?.displayName as string | undefined) || 'Guest',
            isHost: !!s?.data?.isHost
          };
        });
      socket.emit('session-participants', { sessionId, participants });

      if (userId && sessionHosts.get(sessionId) === userId) {
        socket.data.isHost = true;
        console.log(`Host ${userId} rejoined session ${sessionId}`);
      }

      setTimeout(() => flushQueue(sessionId), 0);
    });

    socket.on('join-request', async (data: { sessionId: string; viewerId: string; name: string }) => {
      const { sessionId, viewerId, name } = data;

      const meta = await getMeetingMeta(sessionId);
      if (!meta || !meta.isActive) {
        io.to(viewerId).emit('join-rejected', { sessionId, reason: 'Meeting has ended' });
        return;
      }

      const list = pendingRequests.get(sessionId) || [];
      if (!list.find(r => r.viewerId === viewerId)) {
        list.push({ viewerId, name });
        pendingRequests.set(sessionId, list);
      }
      io.to(sessionId).emit('pending-join', { viewerId, name });
    });

    socket.on('approve-join', (data: { sessionId: string; viewerId: string }) => {
      if (!socket.data.isHost) {
        console.warn(`Unauthorized approve-join from ${socket.id}`);
        return;
      }
      const { sessionId, viewerId } = data;
      const list = pendingRequests.get(sessionId) || [];
      const entry = list.find(r => r.viewerId === viewerId) || { viewerId, name: 'Guest' };
      pendingRequests.set(sessionId, list.filter(r => r.viewerId !== viewerId));

      io.to(viewerId).emit('join-approved', { sessionId });
      io.to(sessionId).emit('viewer-connected', { viewerId, name: entry.name, isHost: false });
      io.to(sessionId).emit('pending-requests-updated', { viewerId });

      (async () => {
        try {
          const mRef = doc(db, 'meetings', sessionId);
          const mSnap = await getDoc(mRef);
          if (mSnap.exists()) {
            const d = mSnap.data() as any;
            const participants: Array<{ id: string; name: string; role?: string }> = Array.isArray(d.participants) ? d.participants : [];
            const exists = participants.find(p => p.id === viewerId);
            const updated = exists
              ? participants.map(p => p.id === viewerId ? { ...p, name: entry.name } : p)
              : [...participants, { id: viewerId, name: entry.name, role: 'participant' }];
            await updateDoc(mRef, { participants: updated });
          }
        } catch (e) {
          console.error('Failed to persist participant add', e);
        }
      })();
    });

    socket.on('reject-join', (data: { sessionId: string; viewerId: string }) => {
      if (!socket.data.isHost) return;
      const { sessionId, viewerId } = data;
      const list = pendingRequests.get(sessionId) || [];
      pendingRequests.set(sessionId, list.filter(r => r.viewerId !== viewerId));
      io.to(viewerId).emit('join-rejected', { sessionId });
      io.to(sessionId).emit('pending-requests-updated', { viewerId });
    });

    socket.on('host-command', (data: { sessionId: string; command: string; value?: unknown }) => {
      if (!socket.data.isHost) return;
      socket.to(data.sessionId).emit('peer-command', {
        command: data.command,
        value: data.value,
        sender: socket.id
      });
    });

    socket.on('targeted-command', (data: { sessionId: string; targetId: string; command: string; value?: unknown }) => {
      if (!socket.data.isHost) return;
      io.to(data.targetId).emit('peer-command', {
        command: data.command,
        value: data.value,
        sender: socket.id
      });
    });

    socket.on('join-user', (userId: string) => {
      socket.join(userId);
    });

    socket.on('viewer-connected', (data: { sessionId: string; viewerId: string; name?: string }) => {
      socket.data.displayName = data.name || socket.data.displayName || 'Guest';
      console.log(`Viewer ${data.viewerId} connected to session ${data.sessionId}`);
      socket.to(data.sessionId).emit('viewer-connected', {
        viewerId: data.viewerId,
        name: data.name,
        isHost: !!socket.data.isHost
      });
      const participants = Array.from(io.sockets.adapter.rooms.get(data.sessionId) || [])
        .filter(id => id !== socket.id)
        .map(id => {
          const s = io.sockets.sockets.get(id);
          return {
            viewerId: id,
            name: (s?.data?.displayName as string | undefined) || 'Guest',
            isHost: !!s?.data?.isHost
          };
        });
      io.to(data.sessionId).emit('session-participants', { sessionId: data.sessionId, participants });
    });

    socket.on('get-session-participants', (data: { sessionId: string }) => {
      const sessionId = data?.sessionId || (socket.data.sessionId as string | undefined);
      if (!sessionId) return;
      const participants = Array.from(io.sockets.adapter.rooms.get(sessionId) || [])
        .filter(id => id !== socket.id)
        .map(id => {
          const s = io.sockets.sockets.get(id);
          return {
            viewerId: id,
            name: (s?.data?.displayName as string | undefined) || 'Guest',
            isHost: !!s?.data?.isHost
          };
        });
      socket.emit('session-participants', { sessionId, participants });
    });

    socket.on('viewer-ready', (data: { sessionId: string; viewerId: string }) => {
      console.log(`Viewer ${data.viewerId} ready for WebRTC in session ${data.sessionId}`);
      socket.to(data.sessionId).emit('viewer-ready', { viewerId: data.viewerId });
    });

    socket.on('viewer-watching', (data: { sessionId: string; viewerId: string }) => {
      console.log(`Viewer ${data.viewerId} is now watching`);
      socket.to(data.sessionId).emit('viewer-watching', { viewerId: data.viewerId });
    });

    socket.on('signal', (data: { target: string; signal: unknown; sessionId: string; metadata?: unknown }) => {
      io.to(data.target).emit('signal', {
        signal: data.signal,
        sender: socket.id,
        metadata: data.metadata
      });
    });

    socket.on('chat-message', (data: { sessionId: string; message: string; senderName: string; senderId: string; timestamp: number }) => {
      io.to(data.sessionId).emit('chat-message', data);
    });

    socket.on('end-meeting', (data: { sessionId: string }) => {
      if (!socket.data.isHost) return;
      console.log(`Meeting ${data.sessionId} ended by host`);
      io.to(data.sessionId).emit('meeting-ended', { sessionId: data.sessionId });
      meetingCache.delete(data.sessionId);
      pendingRequests.delete(data.sessionId);
      (async () => {
        try {
          await updateDoc(doc(db, 'meetings', data.sessionId), { isActive: false, endedAt: Date.now() });
        } catch (e) {
          console.error('Failed to persist meeting end', e);
        }
      })();
    });

    socket.on('host-leaving', (data: { sessionId: string }) => {
      if (!socket.data.isHost) return;
      io.to(data.sessionId).emit('host-left', { sessionId: data.sessionId });
    });

    socket.on('update-name', (data: { sessionId: string; viewerId: string; name: string }) => {
      io.to(data.sessionId).emit('name-updated', { viewerId: data.viewerId, name: data.name });
      (async () => {
        try {
          const mRef = doc(db, 'meetings', data.sessionId);
          const mSnap = await getDoc(mRef);
          if (mSnap.exists()) {
            const d = mSnap.data() as any;
            const participants: Array<{ id: string; name: string; role?: string }> = Array.isArray(d.participants) ? d.participants : [];
            const updated = participants.map(p => p.id === data.viewerId ? { ...p, name: data.name } : p);
            await updateDoc(mRef, { participants: updated });
          }
        } catch (e) {
          console.error('Failed to persist name update', e);
        }
      })();
    });

    socket.on('update-role', (data: { sessionId: string; targetId: string; role: string }) => {
      if (!socket.data.isHost) return;
      io.to(data.sessionId).emit('role-updated', { targetId: data.targetId, role: data.role });

      (async () => {
        try {
          const mRef = doc(db, 'meetings', data.sessionId);
          const mSnap = await getDoc(mRef);
          if (mSnap.exists()) {
            const d = mSnap.data() as any;
            const participants: Array<{ id: string; name: string; role?: string }> = Array.isArray(d.participants) ? d.participants : [];
            const updated = participants.map(p => p.id === data.targetId ? { ...p, role: data.role } : p);
            await updateDoc(mRef, { participants: updated });

            const targetSocket = Array.from(io.sockets.sockets.values()).find(s => s.id === data.targetId);
            if (targetSocket) {
              targetSocket.data.isCoHost = data.role === 'co-host';
            }
          }
        } catch (e) {
          console.error('Failed to persist role update', e);
        }
      })();
    });

    socket.on('reaction', (data: { sessionId: string; reaction: string; senderName: string; senderId: string }) => {
      io.to(data.sessionId).emit('reaction', data);
    });

    socket.on('hand-raised', (data: { sessionId: string; raised: boolean }) => {
      const sessionId = data.sessionId || (socket.data.sessionId as string | undefined);
      if (!sessionId) return;
      io.to(sessionId).emit('hand-updated', { viewerId: socket.id, raised: data.raised });
    });

    socket.on('pin-participant', (data: { sessionId: string; targetId: string | null }) => {
      if (!socket.data.isHost && !socket.data.isCoHost) return;
      const sessionId = data.sessionId || (socket.data.sessionId as string | undefined);
      if (!sessionId) return;
      io.to(sessionId).emit('pinned-updated', { targetId: data.targetId });
    });

    socket.on('transfer-host', async (data: { sessionId: string; targetId: string }) => {
      if (!socket.data.isHost) return;
      const sessionId = data.sessionId || (socket.data.sessionId as string | undefined);
      if (!sessionId) return;

      const targetSocket = Array.from(io.sockets.sockets.values()).find(s => s.id === data.targetId);
      if (!targetSocket) {
        socket.emit('host-transfer-error', { reason: 'Target participant not found' });
        return;
      }
      const newHostUserId = targetSocket.data.userId as string | undefined;
      if (!newHostUserId) {
        socket.emit('host-transfer-error', { reason: 'Target user must be signed in to become host' });
        return;
      }

      try {
        const mRef = doc(db, 'meetings', sessionId);
        const mSnap = await getDoc(mRef);
        if (!mSnap.exists()) {
          socket.emit('host-transfer-error', { reason: 'Meeting not found' });
          return;
        }
        const d = mSnap.data() as any;
        const participants: Array<{ id: string; name: string; role?: string }> = Array.isArray(d.participants) ? d.participants : [];
        const targetParticipant = participants.find(p => p.id === data.targetId);
        const newHostName = targetParticipant?.name || d.hostName || 'Host';

        await updateDoc(mRef, { hostId: newHostUserId, hostName: newHostName });
        sessionHosts.set(sessionId, newHostUserId);
        meetingCache.set(sessionId, { ...meetingCache.get(sessionId)!, hostId: newHostUserId });

        socket.data.isHost = false;
        targetSocket.data.isHost = true;

        io.to(sessionId).emit('host-transferred', {
          sessionId,
          newHostUserId,
          newHostName,
          targetSocketId: data.targetId
        });
      } catch (e) {
        console.error('Failed to transfer host', e);
        socket.emit('host-transfer-error', { reason: 'Failed to transfer host' });
      }
    });

    socket.on('disconnect', () => {
      console.log('User disconnected:', socket.id);
      const sessionId = socket.data.sessionId as string | undefined;
      if (sessionId) {
        io.to(sessionId).emit('viewer-left', { viewerId: socket.id });
        const participants = Array.from(io.sockets.adapter.rooms.get(sessionId) || [])
          .filter(id => id !== socket.id)
          .map(id => {
            const s = io.sockets.sockets.get(id);
            return {
              viewerId: id,
              name: (s?.data?.displayName as string | undefined) || 'Guest',
              isHost: !!s?.data?.isHost
            };
          });
        io.to(sessionId).emit('session-participants', { sessionId, participants });
        if (socket.data.isHost) {
          io.to(sessionId).emit('host-left', { sessionId });
        }
        pendingRequests.delete(sessionId);
        (async () => {
          try {
            const mRef = doc(db, 'meetings', sessionId);
            const mSnap = await getDoc(mRef);
            if (mSnap.exists()) {
              const d = mSnap.data() as any;
              const participants: Array<{ id: string; name: string; role?: string }> = Array.isArray(d.participants) ? d.participants : [];
              const updated = participants.filter(p => p.id !== socket.id);
              await updateDoc(mRef, { participants: updated });
            }
          } catch (e) {
            console.error('Failed to persist participant removal', e);
          }
        })();
      }
    });
  });
};
