import { createVoiceChat } from './voicechat.js';

const EMPTY_STATE = Object.freeze({
  enabled: false,
  permission: 'prompt',
  muted: false,
  othersMuted: false,
  openMic: false,
  pttActive: false,
  transmitting: false,
  peers: []
});

function peerId(peer) {
  if (peer && typeof peer === 'object') return peer.id ?? peer.playerId ?? null;
  return peer;
}

export function isVoiceEligible(peer) {
  const id = peerId(peer);
  if (id == null || id === '') return false;
  if (peer && typeof peer === 'object') {
    if (peer.bot === true || peer.isBot === true || peer.spectator === true || peer.isSpectator === true || peer.eligible === false) {
      return false;
    }
    const role = String(peer.role ?? peer.kind ?? peer.type ?? '').toLowerCase();
    if (role === 'bot' || role === 'spectator' || role === 'spec' || role === 'observer') return false;
  }
  return !/(^|[-_:])(bot|spectator|spec|observer)([-_:]|$)/i.test(String(id));
}

function parseMessage(value) {
  if (value && typeof value === 'object') return value;
  if (typeof value !== 'string' || value.length > 262_144) return null;
  try {
    const parsed = JSON.parse(value);
    return parsed && typeof parsed === 'object' ? parsed : null;
  } catch {
    return null;
  }
}

function storedOpenMic(storage) {
  try {
    return storage?.getItem('q4_voice_openmic') === '1';
  } catch {
    return false;
  }
}

export class Q4VoiceSession {
  constructor({
    globalObject = globalThis,
    storage = globalThis.localStorage,
    voiceFactory = createVoiceChat,
    onStatus = () => {},
    log = (...parts) => console.warn('[q4 voice]', ...parts)
  } = {}) {
    this.globalObject = globalObject;
    this.storage = storage;
    this.voiceFactory = voiceFactory;
    this.onStatus = onStatus;
    this.log = log;
    this.voice = null;
    this.selfId = null;
    this.room = null;
    this.available = false;
    this.peerRecords = new Map();
    this.openMic = storedOpenMic(storage);

    const moduleObject = globalObject.Module || (globalObject.Module = {});
    this.network = moduleObject.q4Network || (moduleObject.q4Network = {});
    this.previousOnText = typeof this.network.onText === 'function' ? this.network.onText : null;
    this.boundOnText = (message) => {
      try { this.previousOnText?.(message); } catch (error) { this.log('previous text handler failed', error); }
      this.handleText(message);
    };
    this.network.onText = this.boundOnText;
  }

  setStatusListener(listener) {
    this.onStatus = typeof listener === 'function' ? listener : () => {};
    this.emit();
  }

  snapshot() {
    return {
      ...EMPTY_STATE,
      ...(this.voice?.state || {}),
      peers: [...(this.voice?.state?.peers || [])],
      available: this.available,
      selfId: this.selfId,
      room: this.room
    };
  }

  emit(state = this.voice?.state) {
    try { this.onStatus({ ...this.snapshot(), ...(state || {}) }); } catch { /* HUD cannot break networking. */ }
  }

  networkApi() {
    return this.globalObject.Module?.q4Network || this.network;
  }

  sendSignal(to, payload) {
    const sendText = this.networkApi()?.sendText;
    if (typeof sendText !== 'function') return false;
    try { return sendText({ t: 'voice-signal', to: String(to), payload }) !== false; } catch { return false; }
  }

  recordPeer(peer) {
    const id = peerId(peer);
    if (id == null) return null;
    const normalized = String(id);
    this.peerRecords.set(normalized, peer && typeof peer === 'object' ? { ...peer, id: normalized } : normalized);
    return normalized;
  }

  eligibleId(id) {
    const normalized = String(id);
    return isVoiceEligible(this.peerRecords.get(normalized) ?? normalized);
  }

  createVoice(selfId) {
    this.voice = this.voiceFactory({
      selfId,
      signal: { send: (to, payload) => this.sendSignal(to, payload) },
      isEligible: (id) => this.eligibleId(id),
      openMic: this.openMic,
      pttKey: 'KeyT',
      onStatus: (state) => this.emit(state),
      log: this.log
    });
  }

  resetVoice() {
    try { this.voice?.destroy?.(); } catch { /* best effort */ }
    this.voice = null;
  }

  handleSession(message) {
    const self = message.self ?? message.selfId;
    const selfId = peerId(self);
    const selfDescriptor = message.self ?? {
      id: selfId,
      bot: message.bot,
      isBot: message.isBot,
      spectator: message.spectator,
      isSpectator: message.isSpectator,
      eligible: message.eligible,
      role: message.role,
      kind: message.kind
    };
    const launchType = this.globalObject.Q4Shell?.launch?.type;
    const eligible = launchType === 'multiplayer' && isVoiceEligible(selfDescriptor);
    const changed = this.selfId != null && String(this.selfId) !== String(selfId);
    if (changed) this.resetVoice();
    this.selfId = selfId == null ? null : String(selfId);
    this.room = message.room == null ? null : String(message.room);
    this.available = this.selfId != null && eligible;
    this.peerRecords.clear();

    const peers = Array.isArray(message.peers) ? message.peers : (Array.isArray(message.peerIds) ? message.peerIds : []);
    const ids = [];
    for (const peer of peers) {
      const id = this.recordPeer(peer);
      if (id != null && this.eligibleId(id)) ids.push(id);
    }

    if (!this.available) {
      this.resetVoice();
      this.emit();
      return;
    }
    if (!this.voice) this.createVoice(this.selfId);
    this.voice.setPeers(ids);
    this.emit();
  }

  handlePeer(message) {
    if (!this.voice) return;
    const descriptor = message.peer ?? message;
    const id = this.recordPeer(descriptor);
    if (id == null) return;
    if (message.state === 'left' || message.state === 'removed' || message.state === 'disconnected') {
      this.peerRecords.delete(id);
      this.voice.removePeer(id);
    } else if (this.eligibleId(id)) {
      this.voice.addPeer(id);
    } else {
      this.voice.removePeer(id);
    }
  }

  handleText(value) {
    const message = parseMessage(value);
    if (!message) return false;
    if (message.t === 'voice-session') this.handleSession(message);
    else if (message.t === 'voice-peer') this.handlePeer(message);
    else if (message.t === 'voice-signal' && this.voice && this.eligibleId(message.from)) {
      this.voice.handleSignal(String(message.from), message.payload);
    } else return false;
    return true;
  }

  async enable() {
    if (!this.available || !this.voice) return false;
    return this.voice.enable();
  }

  disable() { this.voice?.disable(); }
  mute(value) { return this.voice?.mute(value); }
  muteOthers(value) { return this.voice?.muteOthers(value); }
  pttDown() { this.voice?.pttDown(); }
  pttUp() { this.voice?.pttUp(); }

  setOpenMic(value) {
    this.openMic = Boolean(value);
    try { this.storage?.setItem('q4_voice_openmic', this.openMic ? '1' : '0'); } catch { /* preference is optional */ }
    this.voice?.setOpenMic(this.openMic);
    this.emit();
  }

  destroy() {
    this.resetVoice();
    if (this.network.onText === this.boundOnText) this.network.onText = this.previousOnText;
    this.available = false;
    this.emit();
  }
}

export function installQ4VoiceHud({
  globalObject = globalThis,
  documentObject = globalThis.document,
  storage = globalThis.localStorage,
  voiceFactory = createVoiceChat,
  notify = () => {}
} = {}) {
  const byId = (id) => documentObject?.getElementById?.(id) || null;
  const toggle = byId('voiceButton');
  const panel = byId('voicePanel');
  const status = byId('voiceStatus');
  const peerStatus = byId('voicePeers');
  const disable = byId('voiceDisableButton');
  const muteSelf = byId('voiceMuteSelf');
  const muteOthers = byId('voiceMuteOthers');
  const pttMode = byId('voiceModePtt');
  const openMode = byId('voiceModeOpen');
  const touchPtt = documentObject?.querySelector?.('[data-action="ptt"]') || null;
  const abortController = new AbortController();
  const options = { signal: abortController.signal };
  let busy = false;

  const session = new Q4VoiceSession({ globalObject, storage, voiceFactory });

  function render(state) {
    const available = Boolean(state.available);
    const enabled = Boolean(state.enabled);
    const denied = state.permission === 'denied';
    const connected = state.peers.filter((peer) => peer.connected).length;
    const heard = state.peers.filter((peer) => peer.hasAudio).length;

    if (toggle) {
      toggle.disabled = !available || busy;
      toggle.dataset.state = state.transmitting ? 'live' : (enabled ? 'on' : (denied ? 'denied' : 'off'));
      toggle.setAttribute('aria-pressed', String(enabled));
      toggle.title = !available ? 'Czat głosowy jest dostępny w multiplayerze'
        : enabled ? 'Ustawienia czatu głosowego' : denied ? 'Brak dostępu do mikrofonu' : 'Włącz czat głosowy';
      toggle.setAttribute('aria-label', toggle.title);
    }
    if (panel && !available) panel.hidden = true;
    if (status) status.textContent = denied ? 'Brak dostępu do mikrofonu'
      : enabled ? (state.transmitting ? 'Nadawanie' : (state.openMic ? 'Otwarty mikrofon' : 'Push-to-talk'))
        : 'Mikrofon wyłączony';
    if (peerStatus) peerStatus.textContent = state.peers.length
      ? `${connected}/${state.peers.length} połączonych · ${heard} z dźwiękiem`
      : 'Brak innych graczy';

    for (const control of [disable, muteSelf, muteOthers, pttMode, openMode]) {
      if (control) control.disabled = !enabled;
    }
    if (muteSelf) {
      muteSelf.dataset.active = String(state.muted);
      muteSelf.setAttribute('aria-pressed', String(state.muted));
      muteSelf.title = state.muted ? 'Włącz własny mikrofon' : 'Wycisz własny mikrofon';
      muteSelf.setAttribute('aria-label', muteSelf.title);
    }
    if (muteOthers) {
      muteOthers.dataset.active = String(state.othersMuted);
      muteOthers.setAttribute('aria-pressed', String(state.othersMuted));
      muteOthers.title = state.othersMuted ? 'Włącz głos innych graczy' : 'Wycisz innych graczy';
      muteOthers.setAttribute('aria-label', muteOthers.title);
    }
    if (pttMode) {
      pttMode.dataset.active = String(!state.openMic);
      pttMode.setAttribute('aria-pressed', String(!state.openMic));
    }
    if (openMode) {
      openMode.dataset.active = String(state.openMic);
      openMode.setAttribute('aria-pressed', String(state.openMic));
    }
    if (touchPtt) {
      touchPtt.disabled = !enabled || state.openMic;
      touchPtt.dataset.voiceLive = String(state.transmitting);
      touchPtt.setAttribute('aria-label', state.transmitting ? 'Nadawanie głosu' : 'Mów');
    }
  }

  session.setStatusListener(render);

  toggle?.addEventListener('click', async () => {
    if (!session.available || busy) return;
    if (session.voice?.state.enabled) {
      if (panel) panel.hidden = !panel.hidden;
      return;
    }
    busy = true;
    render(session.snapshot());
    if (panel) panel.hidden = false;
    try {
      const enabled = await session.enable();
      if (!enabled) notify(session.voice?.state.permission === 'denied'
        ? 'Przeglądarka nie udzieliła dostępu do mikrofonu.'
        : 'Mikrofon jest niedostępny w tej przeglądarce.', 'error');
    } finally {
      busy = false;
      render(session.snapshot());
    }
  }, options);

  disable?.addEventListener('click', () => {
    session.disable();
    if (panel) panel.hidden = true;
  }, options);
  muteSelf?.addEventListener('click', () => session.mute(), options);
  muteOthers?.addEventListener('click', () => session.muteOthers(), options);
  pttMode?.addEventListener('click', () => session.setOpenMic(false), options);
  openMode?.addEventListener('click', () => session.setOpenMic(true), options);
  globalObject.addEventListener?.('q4-ptt', (event) => {
    if (event.detail?.active) session.pttDown();
    else session.pttUp();
  }, options);
  globalObject.addEventListener?.('pagehide', () => session.destroy(), options);

  return {
    session,
    render,
    destroy() {
      abortController.abort();
      session.destroy();
      if (panel) panel.hidden = true;
    }
  };
}
