import { create } from 'zustand';
import {
  Room,
  RoomEvent,
  Track,
  AudioPresets,
  type RemoteAudioTrack,
  type RemoteParticipant,
  type Participant,
  type TrackPublication,
} from 'livekit-client';
import { api } from '../lib/api.ts';
import { useWebSocketStore } from './ws.ts';
import { BackgroundProcessor } from '@livekit/track-processors';


export interface VoiceParticipant {
  identity: string;
  username: string;
  isMuted: boolean;
  isSpeaking: boolean;
  hasVideo: boolean;
  isScreenSharing: boolean;
}

export interface WhisperNotification {
  fromUserId: string;
  fromUsername: string;
  message: string;
  timestamp: number;
}

interface VoiceTokenResponse {
  token: string;
  livekit_url: string;
}

interface VoiceState {
  currentRoom: string | null;
  currentRoomName: string | null;
  isConnected: boolean;
  isMuted: boolean;
  isDeafened: boolean;
  isVideoOn: boolean;
  isScreenSharing: boolean;
  noiseSuppression: boolean;
  bgMode: 'disabled' | 'background-blur' | 'virtual-background';
  _bgProcessor: any;
  participants: VoiceParticipant[];
  error: string | null;
  _room: Room | null;
  isJoining: boolean;
  whispers: WhisperNotification[];
  joinVoice: (channelId: string, channelName: string) => Promise<void>;
  leaveVoice: () => Promise<void>;
  toggleMute: () => void;
  toggleDeafen: () => void;
  toggleVideo: () => Promise<void>;
  toggleScreenShare: () => Promise<void>;
  toggleNoiseSuppression: () => void;
  setBgMode: (mode: 'disabled' | 'background-blur' | 'virtual-background', imagePath?: string) => Promise<void>;
  _addWhisper: (w: WhisperNotification) => void;
  whisperTo: (targetUserId: string, targetUsername: string, message?: string) => void;
  dismissWhisper: (timestamp: number) => void;
}

function participantToVoice(p: Participant): VoiceParticipant {
  const micPub = p.getTrackPublication(Track.Source.Microphone);
  const camPub = p.getTrackPublication(Track.Source.Camera);
  const screenPub = p.getTrackPublication(Track.Source.ScreenShare);
  const screenAudioPub = p.getTrackPublication(Track.Source.ScreenShareAudio);
  return {
    identity: p.identity,
    username: p.name || p.identity,
    isMuted: micPub?.isMuted ?? true,
    isSpeaking: p.isSpeaking,
    hasVideo: !!(camPub && !camPub.isMuted),
    isScreenSharing: !!(screenPub && !screenPub.isMuted) || !!(screenAudioPub && !screenAudioPub.isMuted),
  };
}

export const useVoiceStore = create<VoiceState>((set, get) => ({
  currentRoom: null,
  currentRoomName: null,
  isConnected: false,
  isMuted: false,
  isDeafened: false,
  isVideoOn: false,
  isScreenSharing: false,
  noiseSuppression: true,
  bgMode: 'disabled',
  _bgProcessor: null,
  participants: [],
  error: null,
  _room: null,
  isJoining: false,
  whispers: [],

  joinVoice: async (channelId, channelName) => {
    if (get().isJoining) {
      console.log("[voice] already joining, ignoring duplicate click");
      return;
    }
    set({ isJoining: true });
    console.log("[voice] joinVoice called:", channelId);
    const existing = get()._room;
    if (existing) {
      await get().leaveVoice();
    }

    set({ error: null });

    try {
      console.log("[voice] requesting token for:", channelId);
      const resp = await api.post<VoiceTokenResponse>('/voice/token', {
        room_name: channelId,
      });
      console.log("[voice] got token response:", resp);

      const room = new Room({
        adaptiveStream: true,
        dynacast: true,
        publishDefaults: {
          red: false,
          dtx: false,
          audioPreset: AudioPresets.speech,
        },
        audioCaptureDefaults: {
          channelCount: 1,
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true,
        },
      });

      room.on(RoomEvent.Connected, () => {
        set({ isConnected: true, error: null });
        syncParticipants();
      });

      room.on(RoomEvent.Disconnected, () => {
        set({
          isConnected: false,
          currentRoom: null,
          currentRoomName: null,
          participants: [],
          isMuted: false,
          isDeafened: false,
          isVideoOn: false,
          isScreenSharing: false,
        });
      });

      room.on(RoomEvent.ParticipantConnected, () => {
        syncParticipants();
      });

      room.on(RoomEvent.ParticipantDisconnected, () => {
        syncParticipants();
      });

      room.on(RoomEvent.TrackMuted, () => {
        syncParticipants();
      });

      room.on(RoomEvent.TrackUnmuted, () => {
        syncParticipants();
      });

      room.on(RoomEvent.TrackPublished, () => syncParticipants());
      room.on(RoomEvent.TrackUnpublished, () => syncParticipants());
      room.on(RoomEvent.TrackSubscribed, () => syncParticipants());
      room.on(RoomEvent.TrackUnsubscribed, () => syncParticipants());
      room.on(RoomEvent.LocalTrackPublished, () => syncParticipants());
      room.on(RoomEvent.LocalTrackUnpublished, () => syncParticipants());

      room.on(
        RoomEvent.ActiveSpeakersChanged,
        (speakers: Participant[]) => {
          set((state) => ({
            participants: state.participants.map((p) => ({
              ...p,
              isSpeaking: speakers.some((s) => s.identity === p.identity),
            })),
          }));
        },
      );

      function syncParticipants() {
        const r = get()._room;
        if (!r) return;

        const all: Participant[] = [
          r.localParticipant,
          ...Array.from(r.remoteParticipants.values()),
        ];
        set({ participants: all.map(participantToVoice) });
      }

      await room.connect(resp.livekit_url, resp.token);

      // Set state immediately so the UI renders the voice panel
      set({
        _room: room,
        currentRoom: channelId,
        currentRoomName: channelName,
        error: null,
      });

      // Mute mic by default on join (best effort; don't fail join if this errors)
      try {
        await room.localParticipant.setMicrophoneEnabled(false);
        set({ isMuted: true });
      } catch (micErr) {
        console.warn("[voice] failed to set initial mute state:", micErr);
      }

      set({ isJoining: false });

      // Notify WebSocket peers
      const wsSend = useWebSocketStore.getState().send;
      wsSend({
        type: 'VOICE_JOIN',
        data: { room_name: channelId },
      });
    } catch (err) {
      set({
        error: err instanceof Error ? err.message : 'Failed to join voice',
      });
    }
  },

  leaveVoice: async () => {
    const room = get()._room;
    if (!room) return;

    const channelId = get().currentRoom;

    const wsSend = useWebSocketStore.getState().send;
    if (channelId) {
      wsSend({
        type: 'VOICE_LEAVE',
        data: { room_name: channelId },
      });
    }

    await room.disconnect();
    set({
      _room: null,
      currentRoom: null,
      currentRoomName: null,
      isConnected: false,
      isMuted: false,
      isDeafened: false,
      isVideoOn: false,
      isScreenSharing: false,
      participants: [],
      error: null,
    });
  },

  toggleMute: () => {
    const room = get()._room;
    if (!room) return;

    const newMuted = !get().isMuted;
    room.localParticipant.setMicrophoneEnabled(!newMuted);
    set({ isMuted: newMuted });

    // If un-deafening while unmuting, also restore speaker
    if (!newMuted && get().isDeafened) {
      set({ isDeafened: false });
    }

    const wsSend = useWebSocketStore.getState().send;
    wsSend({
      type: 'VOICE_MUTE',
      data: { room_name: get().currentRoom, muted: newMuted },
    });
  },

  toggleDeafen: () => {
    const room = get()._room;
    if (!room) return;

    const newDeafened = !get().isDeafened;

    if (newDeafened) {
      // Deafen: mute mic too
      room.localParticipant.setMicrophoneEnabled(false);
      set({ isDeafened: true, isMuted: true });
    } else {
      set({ isDeafened: false });
    }

    // Mute/unmute all remote audio tracks
    room.remoteParticipants.forEach((p: RemoteParticipant) => {
      p.audioTrackPublications.forEach((pub: TrackPublication) => {
        if (pub.track && pub.kind === Track.Kind.Audio) {
          (pub.track as RemoteAudioTrack).setVolume(newDeafened ? 0 : 1.0);
        }
      });
    });

    const wsSend = useWebSocketStore.getState().send;
    wsSend({
      type: 'VOICE_DEAFEN',
      data: { room_name: get().currentRoom, deafened: newDeafened },
    });
  },

  toggleVideo: async () => {
    const room = get()._room;
    if (!room) return;

    const newState = !get().isVideoOn;
    await room.localParticipant.setCameraEnabled(newState);
    set({ isVideoOn: newState });

    if (newState && get().bgMode !== 'disabled') {
      const mode = get().bgMode;
      // re-trigger setBgMode to re-attach processor to the new track
      await get().setBgMode(mode);
    }
  },

  setBgMode: async (mode: 'disabled' | 'background-blur' | 'virtual-background', imagePath?: string) => {
    const room = get()._room;
    if (!room) return;

    let processor = get()._bgProcessor;
    if (!processor && mode !== 'disabled') {
      try {
        processor = BackgroundProcessor({ mode: 'background-blur', blurRadius: 10 });
        set({ _bgProcessor: processor });
      } catch (err) {
        console.warn('Failed to initialize BackgroundProcessor:', err);
        return;
      }
    }

    if (processor) {
      try {
        if (mode === 'disabled') {
          await processor.switchTo({ mode: 'disabled' });
        } else if (mode === 'background-blur') {
          await processor.switchTo({ mode: 'background-blur', blurRadius: 10 });
        } else if (mode === 'virtual-background') {
          await processor.switchTo({ mode: 'virtual-background', imagePath: imagePath || 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809' });
        }

        // attach to camera track if it's running
        const pub = room.localParticipant.getTrackPublication(Track.Source.Camera);
        const track = pub?.track as any;
        if (track && track.setProcessor) {
          await track.setProcessor(processor);
        }
        set({ bgMode: mode });
      } catch (err) {
        console.error('[voice] setBgMode failed:', err);
        alert(`Failed to set background effect: ${err instanceof Error ? err.message : err}`);
        set({ bgMode: 'disabled' });
      }
    } else {
      set({ bgMode: mode });
    }
  },

  toggleScreenShare: async () => {
    const room = get()._room;
    if (!room) return;

    const newState = !get().isScreenSharing;
    try {
      await room.localParticipant.setScreenShareEnabled(newState, { audio: true });
      set({ isScreenSharing: newState });
    } catch (err: any) {
      // user cancelled the browser picker — not an error
      if (err?.name === 'AbortError' || err?.message?.includes('cancel')) return;
      console.error('[voice] screenshare toggle failed:', err);
      set({ error: err instanceof Error ? err.message : 'Screen share failed' });
    }
  },

  toggleNoiseSuppression: async () => {
    const room = get()._room;
    if (!room) return;

    const newState = !get().noiseSuppression;
    set({ noiseSuppression: newState });

    // Apply noise suppression + echo cancellation to local audio track
    try {
      const audioTrack = room.localParticipant.getTrackPublication(Track.Source.Microphone);
      if (audioTrack?.track?.mediaStreamTrack) {
        await audioTrack.track.mediaStreamTrack.applyConstraints({
          echoCancellation: newState,
          noiseSuppression: newState,
          autoGainControl: newState,
        } as MediaTrackConstraints);
      }
    } catch (err) {
      console.warn('[voice] failed to update audio processing:', err);
    }
  },

  _addWhisper: (w) => {
    set((state) => ({ whispers: [...state.whispers, w] }));
  },

  whisperTo: (targetUserId, targetUsername, message) => {
    const wsSend = useWebSocketStore.getState().send;
    wsSend({
      type: 'VOICE_WHISPER',
      data: {
        target_user_id: targetUserId,
        target_username: targetUsername,
        message: message || '',
      },
    });
  },

  dismissWhisper: (timestamp) => {
    set((state) => ({
      whispers: state.whispers.filter((w) => w.timestamp !== timestamp),
    }));
  },
}));
