/**
 * Gespraechsmodus / Wake Word Service
 *
 * Wake-Word-Engine: openWakeWord (https://github.com/dscripka/openWakeWord),
 * komplett on-device via ONNX Runtime in Native-Kotlin (siehe
 * OpenWakeWordModule.kt + assets/openwakeword/). Kein API-Key, kein Cloud-
 * Roundtrip, kein Cent Lizenzgebuehren.
 *
 * Drei Zustaende:
 *   off        — Ohr aus, nichts laeuft
 *   armed      — Ohr aktiv, openWakeWord hoert passiv auf das Wake-Word.
 *                Das Mikro ist von OpenWakeWord belegt; AudioRecorder ist aus.
 *   conversing — Wake-Word getriggert (oder Ohr-Tap manuell):
 *                aktive Konversation. OpenWakeWord pausiert (gibt Mikro frei),
 *                AudioRecorder uebernimmt fuer die Aufnahme.
 *                Nach jeder ARIA-Antwort oeffnet das Mikro fuer X Sekunden
 *                (Conversation-Window). Stille im Fenster → zurueck zu armed.
 *
 * Faellt das Native-Modul aus (alte App-Version, ONNX-Init-Fehler), geht
 * 'start' direkt in 'conversing' (klassischer Direkt-Aufnahme-Modus).
 */

import { NativeEventEmitter, NativeModules, ToastAndroid } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { acquireBackgroundAudio } from './backgroundAudio';

type WakeWordCallback = () => void;
type StateCallback = (state: WakeWordState) => void;

export type WakeWordState = 'off' | 'armed' | 'conversing';

export const WAKE_KEYWORD_STORAGE = 'aria_wake_keyword';

/** Verfuegbare Wake-Words — entsprechen den .onnx Dateien in
 *  android/app/src/main/assets/openwakeword/. Custom-Keywords (eigenes
 *  Training via openwakeword Notebook) muessen aktuell als Asset eingebaut
 *  werden — Diagnostic-Upload ist Phase 2. */
export const WAKE_KEYWORDS = [
  'hey_jarvis',
  'computer',
  'alexa',
  'hey_mycroft',
  'hey_rhasspy',
] as const;
export type WakeKeyword = typeof WAKE_KEYWORDS[number];
export const DEFAULT_KEYWORD: WakeKeyword = 'hey_jarvis';

/** Hilfs-Mapping fuer die Anzeige im UI. */
export const KEYWORD_LABELS: Record<WakeKeyword, string> = {
  hey_jarvis: 'Hey Jarvis',
  computer: 'Computer',
  alexa: 'Alexa',
  hey_mycroft: 'Hey Mycroft',
  hey_rhasspy: 'Hey Rhasspy',
};

// Detection-Tuning — kann in Settings spaeter konfigurierbar werden.
const DEFAULT_THRESHOLD = 0.5;
const DEFAULT_PATIENCE = 2;
const DEFAULT_DEBOUNCE_MS = 1500;

interface OpenWakeWordModule {
  init(modelName: string, threshold: number, patience: number, debounceMs: number): Promise<boolean>;
  start(): Promise<boolean>;
  stop(): Promise<boolean>;
  dispose(): Promise<boolean>;
  isAvailable(): Promise<boolean>;
}

const { OpenWakeWord } = NativeModules as { OpenWakeWord?: OpenWakeWordModule };

class WakeWordService {
  private state: WakeWordState = 'off';
  private wakeCallbacks: WakeWordCallback[] = [];
  private stateCallbacks: StateCallback[] = [];
  /** Barge-In-Callbacks: feuern wenn Wake-Word WAEHREND ARIA spricht erkannt
   *  wird. ChatScreen reagiert mit TTS-stop + neuer Aufnahme. */
  private bargeCallbacks: WakeWordCallback[] = [];
  /** True solange Wake-Word parallel zu TTS aktiv ist. */
  private bargeListening: boolean = false;
  /** Anruf-Pause: state wird gemerkt damit nach Auflegen wiederhergestellt wird. */
  private callPaused: boolean = false;
  private preCallState: WakeWordState = 'off';
  /** Cooldown nach App-Resume: kurze Phase in der Wake-Word-Detections
   *  ignoriert werden. Beim Wechsel von Background nach Vordergrund gibt's
   *  oft einen Audio-Pegel-Spike (AudioFocus-Switch, AudioTrack re-route),
   *  der openWakeWord faelschlich triggern kann. */
  private cooldownUntilMs: number = 0;

  private keyword: WakeKeyword = DEFAULT_KEYWORD;
  private nativeReady: boolean = false;
  private initInProgress: Promise<boolean> | null = null;
  private eventSub: { remove: () => void } | null = null;

  /** Beim App-Start aufrufen — laedt Settings, baut Native-Modul. */
  async loadFromStorage(): Promise<void> {
    try {
      const w = await AsyncStorage.getItem(WAKE_KEYWORD_STORAGE);
      const wt = (w || DEFAULT_KEYWORD).trim() as WakeKeyword;
      this.keyword = (WAKE_KEYWORDS as readonly string[]).includes(wt) ? wt : DEFAULT_KEYWORD;
      await this.initNative();
    } catch (err) {
      console.warn('[WakeWord] loadFromStorage', err);
    }
  }

  /** Settings-Wechsel: anderes Wake-Word. Re-Init des Native-Moduls. */
  async configure(keyword: string): Promise<boolean> {
    const next: WakeKeyword = (WAKE_KEYWORDS as readonly string[]).includes(keyword)
      ? (keyword as WakeKeyword)
      : DEFAULT_KEYWORD;
    this.keyword = next;
    await AsyncStorage.setItem(WAKE_KEYWORD_STORAGE, next);

    // Laufende Instanz stoppen + neu initialisieren
    await this.disposeNative();
    const ok = await this.initNative();
    if (!ok) {
      ToastAndroid.show(
        `Wake-Word "${KEYWORD_LABELS[next]}" konnte nicht initialisiert werden — Logs pruefen`,
        ToastAndroid.LONG,
      );
    }
    return ok;
  }

  private async initNative(): Promise<boolean> {
    if (!OpenWakeWord) {
      console.warn('[WakeWord] OpenWakeWord Native-Modul nicht verfuegbar — Direkt-Aufnahme-Fallback aktiv');
      this.nativeReady = false;
      return false;
    }
    if (this.initInProgress) return this.initInProgress;
    this.initInProgress = (async () => {
      try {
        await OpenWakeWord.init(this.keyword, DEFAULT_THRESHOLD, DEFAULT_PATIENCE, DEFAULT_DEBOUNCE_MS);
        // Subscribe nur einmal
        if (!this.eventSub) {
          const emitter = new NativeEventEmitter(NativeModules.OpenWakeWord);
          this.eventSub = emitter.addListener('WakeWordDetected', () => {
            console.log('[WakeWord] Native Detection-Event empfangen');
            this.onWakeDetected().catch(err =>
              console.warn('[WakeWord] onWakeDetected crashed:', err));
          });
        }
        this.nativeReady = true;
        console.log('[WakeWord] Init OK (model=%s)', this.keyword);
        return true;
      } catch (err: any) {
        console.warn('[WakeWord] Init fehlgeschlagen:', err?.message || err);
        this.nativeReady = false;
        return false;
      } finally {
        this.initInProgress = null;
      }
    })();
    return this.initInProgress;
  }

  private async disposeNative(): Promise<void> {
    if (!OpenWakeWord) return;
    try { await OpenWakeWord.dispose(); } catch {}
    this.nativeReady = false;
  }

  /** Ohr-Button gedrueckt — startet passives Lauschen oder direkt Konversation. */
  async start(): Promise<boolean> {
    if (this.state !== 'off') return true;
    // Foreground-Service VOR dem Mic-Zugriff hochziehen damit Background-
    // Lauschen funktioniert (Android braucht foregroundServiceType=microphone
    // aktiv zum Zeitpunkt des AudioRecord.startRecording).
    await acquireBackgroundAudio('wake');
    if (this.nativeReady && OpenWakeWord) {
      try {
        await OpenWakeWord.start();
        console.log('[WakeWord] armed — warte auf "%s"', this.keyword);
        ToastAndroid.show(`Lausche auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
        this.setState('armed');
        return true;
      } catch (err: any) {
        console.warn('[WakeWord] start fehlgeschlagen — Fallback Direkt-Aufnahme:',
                     err?.message || err);
        ToastAndroid.show(
          `Wake-Word-Start failed: ${err?.message || err}`,
          ToastAndroid.LONG,
        );
      }
    } else {
      console.warn('[WakeWord] Native-Modul nicht bereit — Direkt-Aufnahme-Fallback');
      ToastAndroid.show(
        'Wake-Word nicht aktiv — direkte Aufnahme startet (Mikro hoert mit)',
        ToastAndroid.LONG,
      );
    }
    // Fallback: direkt in Konversation
    console.log('[WakeWord] Direkt-Aufnahme startet (kein Wake-Word)');
    this.setState('conversing');
    setTimeout(() => {
      if (this.state === 'conversing') {
        this.wakeCallbacks.forEach(cb => cb());
      }
    }, 500);
    return true;
  }

  /** Komplett ausschalten (Ohr abschalten) */
  async stop(): Promise<void> {
    console.log('[WakeWord] Ohr deaktiviert');
    if (this.nativeReady && OpenWakeWord) {
      try { await OpenWakeWord.stop(); } catch {}
    }
    this.bargeListening = false;
    this.setState('off');
  }

  /** Cooldown setzen — alle Wake-Word-Detections in den naechsten ms ignorieren.
   *  Wird beim App-Resume gerufen weil AppState-Wechsel Audio-Spikes erzeugen
   *  die openWakeWord faelschlich als Trigger interpretiert. */
  setResumeCooldown(ms: number = 1500): void {
    this.cooldownUntilMs = Date.now() + ms;
    console.log('[WakeWord] Cooldown aktiv fuer %dms', ms);
  }

  /** Wake-Word getriggert: Native-Modul pausieren, Konversation starten. */
  private async onWakeDetected(): Promise<void> {
    const now = Date.now();
    if (now < this.cooldownUntilMs) {
      const left = this.cooldownUntilMs - now;
      console.log('[WakeWord] Trigger ignoriert (Cooldown noch %dms aktiv — wahrscheinlich App-Resume-Spike)', left);
      return;
    }
    console.log('[WakeWord] Wake-Word "%s" erkannt! (state=%s, barge=%s)',
                this.keyword, this.state, this.bargeListening);
    if (this.nativeReady && OpenWakeWord) {
      try { await OpenWakeWord.stop(); } catch {}
    }
    this.bargeListening = false;
    // Wenn wir bereits in 'conversing' sind und der Trigger waehrend ARIAs TTS
    // kam (Barge-In via Wake-Word), feuern wir einen separaten Callback damit
    // ChatScreen das TTS abbrechen + neue Aufnahme starten kann. Sonst normal.
    if (this.state === 'conversing') {
      this.bargeCallbacks.forEach(cb => {
        try { cb(); } catch (e) { console.warn('[WakeWord] barge cb err:', e); }
      });
      // Kein erneutes setState — wir bleiben in 'conversing'.
      return;
    }
    this.setState('conversing');
    setTimeout(() => {
      if (this.state === 'conversing') {
        this.wakeCallbacks.forEach(cb => cb());
      }
    }, 200);
  }

  /** Wake-Word PARALLEL zur TTS-Wiedergabe lauschen lassen — User kann
   *  "Computer" sagen waehrend ARIA noch redet, AcousticEchoCanceler im
   *  Native-Modul verhindert dass ARIAs eigene Stimme triggert.
   *  Voraussetzung: AudioRecorder muss frei sein (Recording aus). Wenn der
   *  AudioRecorder gerade laeuft, hat der Vorrang — Wake-Word geht nicht. */
  async startBargeListening(): Promise<void> {
    if (!this.nativeReady || !OpenWakeWord) return;
    if (this.state !== 'conversing') return;
    if (this.bargeListening) return;
    try {
      await OpenWakeWord.start();
      this.bargeListening = true;
      console.log('[WakeWord] Barge-Listening aktiv (parallel zu TTS)');
    } catch (err) {
      console.warn('[WakeWord] Barge-Listening start fehlgeschlagen:', err);
    }
  }

  /** Barge-Listening wieder aus — z.B. wenn der AudioRecorder fuer die
   *  naechste Aufnahme das Mikro braucht. */
  async stopBargeListening(): Promise<void> {
    if (!this.bargeListening) return;
    if (this.nativeReady && OpenWakeWord) {
      try { await OpenWakeWord.stop(); } catch {}
    }
    this.bargeListening = false;
    console.log('[WakeWord] Barge-Listening aus');
  }

  /** Bei eingehendem Anruf: Wake-Word + Aufnahme stoppen, Pre-Call-State
   *  merken. Telefonie-App belegt das Mikro waehrend des Anrufs, plus ARIA
   *  soll nicht in laufende Telefonate reinhoeren. */
  async pauseForCall(): Promise<void> {
    if (this.callPaused) return;
    this.preCallState = this.state;
    if (this.state === 'off') {
      this.callPaused = true;  // merken dass wir pausiert wurden
      return;
    }
    this.callPaused = true;
    if (this.nativeReady && OpenWakeWord) {
      try { await OpenWakeWord.stop(); } catch {}
    }
    this.bargeListening = false;
    console.log('[WakeWord] Anruf — Wake-Word pausiert (war: %s)', this.preCallState);
  }

  /** Nach Auflegen: Pre-Call-State wiederherstellen. Aktive Konversation
   *  geht zu armed zurueck (User soll nicht in einen halben Dialog springen). */
  async resumeFromCall(): Promise<void> {
    if (!this.callPaused) return;
    const restoreTo = this.preCallState;
    this.callPaused = false;
    this.preCallState = 'off';
    console.log('[WakeWord] Anruf zu Ende — restore state=%s', restoreTo);
    if (restoreTo === 'off') return;
    // Aktive Konversation war wahrscheinlich durch haltAllPlayback eh abgebrochen,
    // sicher zu armed degraden.
    if (restoreTo === 'conversing') this.setState('armed');
    if (this.nativeReady && OpenWakeWord) {
      try { await OpenWakeWord.start(); } catch (err) {
        console.warn('[WakeWord] Restore-Start fehlgeschlagen:', err);
      }
    }
  }

  /** Konversation beenden — User hat im Window nichts gesagt.
   *  Mit Wake-Word: zurueck zu 'armed' (Listener wieder an).
   *  Ohne: zurueck zu 'off'.
   */
  async endConversation(): Promise<void> {
    if (this.state !== 'conversing') return;
    if (this.nativeReady && OpenWakeWord) {
      try {
        await OpenWakeWord.start();
        console.log('[WakeWord] Konversation zu Ende — zurueck zu armed');
        ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
        this.setState('armed');
        return;
      } catch (err) {
        console.warn('[WakeWord] re-arm fehlgeschlagen:', err);
      }
    }
    console.log('[WakeWord] Konversation zu Ende — Ohr aus');
    ToastAndroid.show('Mikro aus', ToastAndroid.SHORT);
    this.setState('off');
  }

  /** Nach ARIA-Antwort (TTS fertig): naechste Aufnahme im Conversation-Window starten */
  async resume(): Promise<void> {
    if (this.state !== 'conversing') return;
    // Kurze Pause damit TTS-Audio nicht ins Mikrofon geht
    await new Promise(resolve => setTimeout(resolve, 800));
    if (this.state === 'conversing') {
      console.log('[WakeWord] TTS fertig — naechste Aufnahme im Conversation-Window');
      this.wakeCallbacks.forEach(cb => cb());
    }
  }

  /** True solange das Ohr aktiv ist (armed ODER conversing). */
  isActive(): boolean {
    return this.state !== 'off';
  }

  isConversing(): boolean {
    return this.state === 'conversing';
  }

  hasWakeWord(): boolean {
    return this.nativeReady;
  }

  getKeyword(): WakeKeyword {
    return this.keyword;
  }

  // --- Callbacks ---

  onWakeWord(callback: WakeWordCallback): () => void {
    this.wakeCallbacks.push(callback);
    return () => {
      this.wakeCallbacks = this.wakeCallbacks.filter(cb => cb !== callback);
    };
  }

  /** Subscribe auf Barge-In-Events: Wake-Word erkannt waehrend ARIA noch
   *  spricht. ChatScreen sollte dann TTS abbrechen + neue Aufnahme starten. */
  onBargeIn(callback: WakeWordCallback): () => void {
    this.bargeCallbacks.push(callback);
    return () => {
      this.bargeCallbacks = this.bargeCallbacks.filter(cb => cb !== callback);
    };
  }

  isBargeListening(): boolean {
    return this.bargeListening;
  }

  onStateChange(callback: StateCallback): () => void {
    this.stateCallbacks.push(callback);
    return () => {
      this.stateCallbacks = this.stateCallbacks.filter(cb => cb !== callback);
    };
  }

  getState(): WakeWordState {
    return this.state;
  }

  private setState(state: WakeWordState): void {
    if (this.state !== state) {
      this.state = state;
      this.stateCallbacks.forEach(cb => cb(state));
    }
  }
}

const wakeWordService = new WakeWordService();
export default wakeWordService;
