/**
 * In-game reactions and quick chat — one implementation for every game.
 *
 * The complaint this exists to fix: reactions did not sync, and when something
 * did appear it floated in the middle of the screen with no indication of who
 * had sent it. Four games had four separate implementations, three of which
 * were broken in a different way.
 *
 * Two ideas carry the whole module:
 *
 *  1. SEATS. A game registers which DOM element belongs to which player, once.
 *     Every reaction is then drawn attached to that player's seat, carrying
 *     their name and avatar. You can always tell who is talking, because the
 *     bubble is physically on them.
 *
 *  2. Reactions travel over `api/match-chat.php`, not through the match's own
 *     game state. Nothing in here can touch a board, a clock or a turn.
 *
 * Usage from a game scene:
 *
 *     reactions.start({ matchId, gameKey, myId });
 *     reactions.seat(myId,  playerBarEl,   { name: 'You',  side: 'bottom' });
 *     reactions.seat(oppId, opponentBarEl, { name: 'Rana', side: 'top' });
 *     reactions.mountPanel(container, toggleButtonEl);
 *     // on unmount:
 *     reactions.stop();
 */

import * as net from './net.js';
import * as audio from './audio.js';
import { t } from './i18n.js';

/** Keys must match REACTION_EMOTES in api/match-chat.php. */
export const EMOTES = [
  { key: 'gg',        emoji: '🤝' },
  { key: 'good_move', emoji: '👏' },
  { key: 'wow',       emoji: '😮' },
  { key: 'laugh',     emoji: '😂' },
  { key: 'think',     emoji: '🤔' },
  { key: 'fire',      emoji: '🔥' },
  { key: 'love',      emoji: '❤️' },
  { key: 'sad',       emoji: '😢' },
  { key: 'angry',     emoji: '😤' },
  { key: 'hurry',     emoji: '⏱️' },
  { key: 'hello',     emoji: '👋' },
  { key: 'salute',    emoji: '🫡' },
];

/** Ids must match REACTION_PHRASES in api/match-chat.php. */
export const PHRASES = [
  'good_luck', 'good_game', 'well_played', 'nice_try', 'close_one',
  'oops', 'sorry', 'thanks', 'hurry_please', 'rematch',
];

const POLL_MS = 2500;
const SEND_COOLDOWN_MS = 2000;
const BUBBLE_MS = 4200;

let session = null;

/** Localised label for a preset phrase, falling back to a readable default. */
export function phraseText(id) {
  const fallback = {
    good_luck: 'Good luck!', good_game: 'Good game', well_played: 'Well played',
    nice_try: 'Nice try', close_one: 'That was close', oops: 'Oops',
    sorry: 'Sorry!', thanks: 'Thanks', hurry_please: 'Your turn ⏱️', rematch: 'Rematch?',
  }[id] || id;
  // t() echoes the key back when a string is missing, so a bare `|| fallback`
  // would never fire — compare against the key itself.
  const key = 'reaction.' + id;
  const translated = t(key);
  return translated === key ? fallback : translated;
}

export function emojiFor(key) {
  return (EMOTES.find(e => e.key === key) || {}).emoji || '💬';
}

// ---------------------------------------------------------------------------
// Session
// ---------------------------------------------------------------------------

export function start({ matchId, gameKey, myId, live = true }) {
  stop();
  session = {
    matchId, gameKey, myId,
    live: live && !!matchId,
    seats: new Map(),        // playerId -> { el, name, avatarUrl, side }
    after: null,             // server timestamp of the newest message we have
    seen: new Set(),         // message ids already shown
    lastSentAt: 0,
    pollTimer: null,
    inFlight: false,
    panel: null,
  };
  ensureStyles();
  if (session.live) startPolling();
  return session;
}

export function stop() {
  if (!session) return;
  clearInterval(session.pollTimer);
  document.querySelectorAll('.rx-bubble').forEach(b => b.remove());
  session.panel?.remove();
  session = null;
}

export function isActive() { return !!session; }

/**
 * Tell the module where a player sits.
 *
 * `side` says which way the bubble should open so it never covers the board:
 * 'top' seats bubble downward, 'bottom' seats bubble upward.
 */
export function seat(playerId, el, { name = '', avatarUrl = null, side = 'top' } = {}) {
  if (!session || !playerId || !el) return;
  session.seats.set(playerId, { el, name, avatarUrl, side });
}

export function updateSeat(playerId, patch) {
  if (!session) return;
  const s = session.seats.get(playerId);
  if (s) session.seats.set(playerId, { ...s, ...patch });
}

// ---------------------------------------------------------------------------
// Sending
// ---------------------------------------------------------------------------

export async function send(kind, key) {
  if (!session) return false;

  const now = Date.now();
  if (now - session.lastSentAt < SEND_COOLDOWN_MS) return false;
  session.lastSentAt = now;

  // Show our own reaction immediately. Waiting for the round trip makes the
  // button feel broken on a slow connection.
  showBubble({
    senderId: session.myId,
    kind,
    key,
    emoji: kind === 'emote' ? emojiFor(key) : null,
    text: kind === 'phrase' ? phraseText(key) : null,
    mine: true,
  });
  audio.play('click');

  if (!session.live) return true;

  try {
    const res = await net.post('match-chat.php', {
      action: 'send',
      match_id: session.matchId,
      game_key: session.gameKey,
      kind, key,
    });
    return !!res?.ok;
  } catch (e) {
    return false;
  }
}

/**
 * Show a reaction that came from nowhere on the network — a bot celebrating,
 * or any purely local flourish. Renders through the same path as a real one so
 * bots and people look identical on screen.
 */
export function showLocal(senderId, kind, key) {
  if (!session) return;
  showBubble({
    senderId,
    kind,
    key,
    emoji: kind === 'emote' ? emojiFor(key) : null,
    text: kind === 'phrase' ? phraseText(key) : null,
    mine: senderId === session.myId,
  });
}

// ---------------------------------------------------------------------------
// Receiving
// ---------------------------------------------------------------------------

function startPolling() {
  session.pollTimer = setInterval(poll, POLL_MS);
  poll();
}

async function poll() {
  if (!session || session.inFlight || document.hidden) return;
  session.inFlight = true;
  try {
    const res = await net.get('match-chat.php', {
      action: 'poll',
      match_id: session.matchId,
      game_key: session.gameKey,
      after: session.after || undefined,
    });
    if (!session || !res) return;
    session.after = res.now || session.after;

    for (const m of (res.messages || [])) {
      if (session.seen.has(m.id)) continue;
      session.seen.add(m.id);
      if (m.mine) continue;   // ours was shown optimistically already
      showBubble({
        senderId: m.sender_id,
        kind: m.kind,
        key: m.key,
        emoji: m.emoji || (m.kind === 'emote' ? emojiFor(m.key) : null),
        text: m.kind === 'phrase' ? phraseText(m.key) : null,
        name: m.name,
        avatarUrl: m.avatar_url,
        mine: false,
      });
      audio.play('notification');
    }
    // The seen set is bounded — a long game must not grow it without limit.
    if (session.seen.size > 200) session.seen = new Set([...session.seen].slice(-100));
  } catch (e) {
    // A reaction that fails to arrive must never disturb the game.
  } finally {
    if (session) session.inFlight = false;
  }
}

// ---------------------------------------------------------------------------
// Presentation
// ---------------------------------------------------------------------------

/**
 * Draw a reaction attached to its sender's seat.
 *
 * Falls back to the top-centre of the viewport only when the game has not
 * registered a seat for that player — which is the case the old code was
 * permanently stuck in.
 */
function showBubble({ senderId, kind, key, emoji, text, name, avatarUrl, mine }) {
  if (!session) return;
  const seatInfo = resolveSeat(senderId, mine);
  const label = name || seatInfo?.name || '';
  const avatar = avatarUrl || seatInfo?.avatarUrl || null;
  const side = seatInfo?.side || 'top';

  const bubble = document.createElement('div');
  bubble.className = `rx-bubble rx-${side}${mine ? ' rx-mine' : ''}`;
  bubble.setAttribute('role', 'status');
  bubble.setAttribute('aria-live', 'polite');

  const who = label
    ? `<span class="rx-who">${escapeHtml(label)}</span>`
    : '';
  const avatarHtml = avatar
    ? `<img class="rx-avatar" src="${escapeHtml(avatar)}" alt="">`
    : `<span class="rx-avatar rx-avatar-blank" aria-hidden="true">${escapeHtml((label || '?').slice(0, 1))}</span>`;

  bubble.innerHTML = kind === 'emote'
    ? `${avatarHtml}<span class="rx-emoji">${emoji}</span>${who}`
    : `${avatarHtml}<span class="rx-text">${escapeHtml(text || '')}</span>${who}`;

  document.body.appendChild(bubble);
  position(bubble, seatInfo, side);

  // Stack rather than overlap when reactions land together.
  offsetForCollisions(bubble);

  requestAnimationFrame(() => bubble.classList.add('rx-in'));
  setTimeout(() => {
    bubble.classList.remove('rx-in');
    bubble.classList.add('rx-out');
    setTimeout(() => bubble.remove(), 260);
  }, BUBBLE_MS);
}

/**
 * Which seat does this sender occupy?
 *
 * An exact id match wins. Failing that, in a two-seat game the reaction can
 * only have come from the seat that is not ours — which lets a game register
 * its opponent seat before it knows the opponent's id, rather than falling back
 * to an unattributed bubble in the middle of the screen.
 */
function resolveSeat(senderId, mine) {
  const exact = session.seats.get(senderId);
  if (exact) return exact;

  if (mine) return session.seats.get(session.myId) || null;

  const others = [...session.seats.entries()].filter(([id]) => id !== session.myId);
  return others.length === 1 ? others[0][1] : null;
}

function position(bubble, seatInfo, side) {
  const gap = 10;
  const bw = bubble.offsetWidth;
  const bh = bubble.offsetHeight;
  const vw = window.innerWidth;
  const vh = window.innerHeight;

  let x, y;
  if (seatInfo?.el?.isConnected) {
    const r = seatInfo.el.getBoundingClientRect();
    x = r.left + r.width / 2 - bw / 2;
    y = side === 'top' ? r.bottom + gap : r.top - bh - gap;
  } else {
    x = vw / 2 - bw / 2;
    y = side === 'top' ? 72 : vh - bh - 96;
  }

  // Keep it fully on screen on the narrowest phone.
  x = Math.max(8, Math.min(x, vw - bw - 8));
  y = Math.max(8, Math.min(y, vh - bh - 8));

  bubble.style.left = `${Math.round(x)}px`;
  bubble.style.top = `${Math.round(y)}px`;
}

function offsetForCollisions(bubble) {
  const others = [...document.querySelectorAll('.rx-bubble')].filter(b => b !== bubble);
  const r = bubble.getBoundingClientRect();
  let shift = 0;
  for (const o of others) {
    const or = o.getBoundingClientRect();
    const overlaps = !(r.right < or.left || r.left > or.right || r.bottom < or.top || r.top > or.bottom);
    if (overlaps) shift += or.height + 6;
  }
  if (shift) {
    const top = parseFloat(bubble.style.top) + shift;
    bubble.style.top = `${Math.min(top, window.innerHeight - r.height - 8)}px`;
  }
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, c =>
    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}

// ---------------------------------------------------------------------------
// Picker
// ---------------------------------------------------------------------------

/**
 * Mount the emote/phrase picker and wire it to a toggle button.
 * The panel is a sheet anchored to the bottom of the screen, which is the only
 * place a thumb can reach comfortably on a tall phone.
 */
export function mountPanel(container, toggleEl) {
  if (!session) return null;
  session.panel?.remove();

  const panel = document.createElement('div');
  panel.className = 'rx-panel';
  panel.hidden = true;
  panel.innerHTML = `
    <div class="rx-panel-grip" aria-hidden="true"></div>
    <div class="rx-row rx-emotes" role="group" aria-label="${escapeHtml(t('reaction.emotes') || 'Reactions')}">
      ${EMOTES.map(e => `
        <button type="button" class="rx-pick rx-pick-emote" data-kind="emote" data-key="${e.key}"
                aria-label="${escapeHtml(e.key)}">${e.emoji}</button>`).join('')}
    </div>
    <div class="rx-row rx-phrases" role="group" aria-label="${escapeHtml(t('reaction.phrases') || 'Quick chat')}">
      ${PHRASES.map(p => `
        <button type="button" class="rx-pick rx-pick-phrase" data-kind="phrase" data-key="${p}">${escapeHtml(phraseText(p))}</button>`).join('')}
    </div>
  `;

  panel.addEventListener('click', async (ev) => {
    const btn = ev.target.closest('.rx-pick');
    if (!btn) return;
    btn.disabled = true;
    await send(btn.dataset.kind, btn.dataset.key);
    close();
    setTimeout(() => { btn.disabled = false; }, SEND_COOLDOWN_MS);
  });

  const open = () => {
    panel.hidden = false;
    toggleEl?.setAttribute('aria-expanded', 'true');
    requestAnimationFrame(() => panel.classList.add('rx-panel-in'));
  };
  const close = () => {
    panel.classList.remove('rx-panel-in');
    toggleEl?.setAttribute('aria-expanded', 'false');
    setTimeout(() => { if (panel.isConnected) panel.hidden = true; }, 200);
  };

  if (toggleEl) {
    toggleEl.setAttribute('aria-haspopup', 'true');
    toggleEl.setAttribute('aria-expanded', 'false');
    toggleEl.addEventListener('click', (e) => {
      e.stopPropagation();
      panel.hidden ? open() : close();
    });
  }
  document.addEventListener('click', (e) => {
    if (!panel.hidden && !panel.contains(e.target) && e.target !== toggleEl) close();
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !panel.hidden) close();
  });

  (container || document.body).appendChild(panel);
  session.panel = panel;
  return { panel, open, close };
}

// ---------------------------------------------------------------------------

let stylesInjected = false;
function ensureStyles() {
  if (stylesInjected || document.getElementById('rx-styles')) { stylesInjected = true; return; }
  const link = document.createElement('link');
  link.id = 'rx-styles';
  link.rel = 'stylesheet';
  link.href = '/public/css/reactions.css';
  document.head.appendChild(link);
  stylesInjected = true;
}
