import { GoogleGenAI, Modality } from '@google/genai';

const API_BASE = '/triviai/api';
const LIVE_MODEL = 'gemini-2.5-flash-native-audio-preview-12-2025';

interface LiveTokenResponse {
  token: string;
  expiresAt?: string;
  sessionStartDeadline?: string;
}

interface SpeakingWindow {
  active: boolean;
  startedAt: number;
  endsAt: number;
}

async function fetchLiveToken(): Promise<LiveTokenResponse> {
  const response = await fetch(`${API_BASE}/live-token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({}),
  });
  const data = await response.json().catch(() => ({}));
  if (!response.ok || typeof data?.token !== 'string' || !data.token) {
    const message = typeof data?.error === 'string' ? data.error : `Token request failed with ${response.status}`;
    throw new Error(message);
  }
  return data as LiveTokenResponse;
}

export class LiveTriviaSession {
  private ai: any;
  private session: any = null;
  private audioContext: AudioContext | null = null;
  private mediaStream: MediaStream | null = null;
  private processor: ScriptProcessorNode | null = null;
  private source: MediaStreamAudioSourceNode | null = null;
  
  // Playback
  private playbackContext: AudioContext | null = null;
  private nextPlayTime: number = 0;
  private currentTurnStartedAt = 0;
  private currentTurnEndsAt = 0;
  private turnHasAudio = false;
  private turnTranscript = '';
  private speakWindowTimeout: number | null = null;

  public onStateChange?: (state: 'connecting' | 'connected' | 'disconnected') => void;
  public onTranscriptChange?: (text: string) => void;
  public onSpeakingWindowChange?: (window: SpeakingWindow) => void;

  constructor(
    private personality: string,
    private setStatusMessage?: (message: string) => void,
  ) {
    this.ai = new GoogleGenAI({
      apiKey: '',
      httpOptions: { apiVersion: 'v1alpha' },
    });
  }

  private emitSpeakingWindow(active: boolean, startedAt?: number, endsAt?: number) {
    this.onSpeakingWindowChange?.({
      active,
      startedAt: startedAt ?? 0,
      endsAt: endsAt ?? 0,
    });
  }

  private clearSpeakWindowTimeout() {
    if (this.speakWindowTimeout !== null) {
      window.clearTimeout(this.speakWindowTimeout);
      this.speakWindowTimeout = null;
    }
  }

  private resetTurnState() {
    this.turnHasAudio = false;
    this.turnTranscript = '';
    this.currentTurnStartedAt = 0;
    this.currentTurnEndsAt = 0;
    this.clearSpeakWindowTimeout();
  }

  async connect() {
    this.onStateChange?.('connecting');
    this.setStatusMessage?.('Requesting a short-lived Gemini live token...');

    const liveToken = await fetchLiveToken();
    this.ai = new GoogleGenAI({
      apiKey: liveToken.token,
      httpOptions: { apiVersion: 'v1alpha' },
    });
    
    this.playbackContext = new AudioContext({ sampleRate: 24000 });
    if (this.playbackContext.state === 'suspended') {
      await this.playbackContext.resume();
    }
    this.nextPlayTime = this.playbackContext.currentTime;

    const systemInstruction = `You are a trivia game host. Your personality is: ${this.personality}.
    You should welcome the player, ask them what topic they want to play, and then start asking them trivia questions one by one.
    Keep track of their score. Be highly conversational, engaging, and stay in character at all times.
    Wait for the user to answer before moving to the next question.`;

    const sessionPromise = this.ai.live.connect({
      model: LIVE_MODEL,
      callbacks: {
        onopen: async () => {
          try {
            this.setStatusMessage?.('Live voice connected. Preparing microphone and host audio...');
            this.audioContext = new AudioContext({ sampleRate: 16000 });
            this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
            this.source = this.audioContext.createMediaStreamSource(this.mediaStream);
            this.processor = this.audioContext.createScriptProcessor(4096, 1, 1);

            this.processor.onaudioprocess = (e) => {
              const inputData = e.inputBuffer.getChannelData(0);
              const pcm16 = new Int16Array(inputData.length);
              for (let i = 0; i < inputData.length; i++) {
                let s = Math.max(-1, Math.min(1, inputData[i]));
                pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
              }
              
              const buffer = new ArrayBuffer(pcm16.length * 2);
              const view = new DataView(buffer);
              for (let i = 0; i < pcm16.length; i++) {
                view.setInt16(i * 2, pcm16[i], true);
              }
              
              let binary = '';
              const bytes = new Uint8Array(buffer);
              for (let i = 0; i < bytes.byteLength; i++) {
                binary += String.fromCharCode(bytes[i]);
              }
              const base64Data = btoa(binary);

              sessionPromise.then((session) => {
                session.sendRealtimeInput({
                  media: { data: base64Data, mimeType: 'audio/pcm;rate=16000' }
                });
              });
            };

            this.source.connect(this.processor);
            this.processor.connect(this.audioContext.destination);
            this.onStateChange?.('connected');
          } catch (err) {
            console.error("Failed to start audio capture", err);
            const message = err instanceof Error && err.message
              ? err.message
              : 'Microphone access failed.';
            this.setStatusMessage?.(`Microphone setup failed: ${message}`);
            this.disconnect();
          }
        },
        onmessage: async (message: any) => {
          if (message.serverContent?.interrupted) {
            this.clearPlaybackQueue();
            this.emitSpeakingWindow(false);
          }

          const outputTranscription = message.serverContent?.outputTranscription;
          const transcriptText = typeof outputTranscription?.text === 'string'
            ? outputTranscription.text.replace(/\s+/g, ' ').trim()
            : '';
          if (transcriptText) {
            if (!this.turnTranscript) {
              this.turnTranscript = transcriptText;
            } else if (transcriptText.startsWith(this.turnTranscript)) {
              this.turnTranscript = transcriptText;
            } else if (!this.turnTranscript.endsWith(transcriptText)) {
              this.turnTranscript = `${this.turnTranscript} ${transcriptText}`.replace(/\s+/g, ' ').trim();
            }
            this.onTranscriptChange?.(this.turnTranscript);
          }

          const parts = message.serverContent?.modelTurn?.parts ?? [];

          for (const part of parts) {
            const base64Audio = part?.inlineData?.data;
            if (base64Audio) {
              this.playAudioChunk(base64Audio);
            }
          }

          if (message.serverContent?.turnComplete) {
            if (this.turnHasAudio && this.currentTurnStartedAt > 0 && this.currentTurnEndsAt > this.currentTurnStartedAt) {
              this.emitSpeakingWindow(true, this.currentTurnStartedAt, this.currentTurnEndsAt);
              const remainingMs = Math.max(0, this.currentTurnEndsAt - Date.now());
              this.clearSpeakWindowTimeout();
              this.speakWindowTimeout = window.setTimeout(() => {
                this.emitSpeakingWindow(false);
              }, remainingMs + 80);
            } else {
              this.emitSpeakingWindow(false);
            }
            this.setStatusMessage?.('Host finished speaking. Answer out loud when you are ready.');
            this.resetTurnState();
          }
        },
        onclose: () => {
          this.setStatusMessage?.('Live voice session ended.');
          this.disconnect();
        },
        onerror: (err) => {
          console.error("Live API Error:", err);
          this.setStatusMessage?.('Live voice hit an error and was closed.');
          this.disconnect();
        }
      },
      config: {
        responseModalities: [Modality.AUDIO],
        outputAudioTranscription: {},
        speechConfig: {
          voiceConfig: { prebuiltVoiceConfig: { voiceName: 'Puck' } },
        },
        systemInstruction,
        sessionResumption: {},
      },
    });

    this.session = await sessionPromise;
    this.setStatusMessage?.('Live voice connected. Asking the host to open the round...');
    this.session.sendClientContent({
      turns: [
        {
          role: 'user',
          parts: [
            {
              text: `Open the live session now. Briefly greet the player in the ${this.personality} persona, ask what trivia topic they want to play, and then wait for the player to answer.`,
            },
          ],
        },
      ],
      turnComplete: true,
    });
  }

  private playAudioChunk(base64Data: string) {
    if (!this.playbackContext) return;
    if (this.playbackContext.state === 'suspended') {
      void this.playbackContext.resume();
    }

    const binaryString = atob(base64Data);
    const len = binaryString.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }

    const float32Data = new Float32Array(bytes.length / 2);
    const dataView = new DataView(bytes.buffer);
    for (let i = 0; i < float32Data.length; i++) {
      const int16 = dataView.getInt16(i * 2, true);
      float32Data[i] = int16 / 32768.0;
    }

    const audioBuffer = this.playbackContext.createBuffer(1, float32Data.length, 24000);
    audioBuffer.getChannelData(0).set(float32Data);

    const source = this.playbackContext.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(this.playbackContext.destination);

    const currentTime = this.playbackContext.currentTime;
    if (this.nextPlayTime < currentTime) {
      this.nextPlayTime = currentTime;
    }

    if (!this.turnHasAudio) {
      this.turnHasAudio = true;
      this.currentTurnStartedAt = Date.now() + Math.max(0, (this.nextPlayTime - currentTime) * 1000);
    }

    source.start(this.nextPlayTime);
    this.nextPlayTime += audioBuffer.duration;
    this.currentTurnEndsAt = Date.now() + Math.max(0, (this.nextPlayTime - currentTime) * 1000);
    this.emitSpeakingWindow(true, this.currentTurnStartedAt, this.currentTurnEndsAt);
  }

  private clearPlaybackQueue() {
    if (this.playbackContext) {
      this.nextPlayTime = this.playbackContext.currentTime;
    }
    this.clearSpeakWindowTimeout();
    this.turnHasAudio = false;
    this.currentTurnStartedAt = 0;
    this.currentTurnEndsAt = 0;
  }

  disconnect() {
    this.clearSpeakWindowTimeout();
    if (this.session) {
      try {
        if (typeof this.session.close === 'function') {
           this.session.close();
        }
      } catch (e) {}
      this.session = null;
    }
    if (this.processor) {
      this.processor.disconnect();
      this.processor = null;
    }
    if (this.source) {
      this.source.disconnect();
      this.source = null;
    }
    if (this.mediaStream) {
      this.mediaStream.getTracks().forEach(t => t.stop());
      this.mediaStream = null;
    }
    if (this.audioContext) {
      this.audioContext.close();
      this.audioContext = null;
    }
    if (this.playbackContext) {
      this.playbackContext.close();
      this.playbackContext = null;
    }
    this.resetTurnState();
    this.emitSpeakingWindow(false);
    this.onStateChange?.('disconnected');
  }
}
