// irc.jsx
// Browser-side IRC client for the terminal landing.
//
// Exports two globals (no module system — see CLAUDE.md):
//   window.useIrc(opts)  – React hook that owns the WebSocket connection.
//   window.IrcView({irc})– Renders the message log.
//
// Wire protocol: see party/chat.ts. All frames are JSON.
//
// Connection lifecycle:
//   - Hook mounts, opens a socket, exponentially backs off on close.
//   - On the first 'hello', the server-assigned nick replaces our local guess.
//   - Local "system" lines (from /help, /who, connection errors) are inserted
//     into the same message buffer as server frames so the UI is one list.

const NICK_STORAGE = 'bromb-irc-nick';
const MSG_BUFFER_MAX = 200;
const RECONNECT_MIN_MS = 500;
const RECONNECT_MAX_MS = 15000;

function loadStoredNick() {
  try {
    const v = window.localStorage.getItem(NICK_STORAGE);
    return v && /^[A-Za-z0-9_-]{1,16}$/.test(v) ? v : null;
  } catch { return null; }
}
function saveStoredNick(nick) {
  try { window.localStorage.setItem(NICK_STORAGE, nick); } catch {}
}

// Stable client-side ids for locally-injected system lines.
let __localSeq = 0;
function localId() { return `local-${++__localSeq}`; }

function useIrc({ url, channel = '#lobby' }) {
  const [messages, setMessages]   = React.useState([]); // array of frames
  const [presence, setPresence]   = React.useState([]);
  const [status, setStatus]       = React.useState('connecting'); // connecting | online | reconnecting | offline
  const [nick, setNick]           = React.useState(() => loadStoredNick() || 'guest');

  const wsRef        = React.useRef(null);
  const reconnectRef = React.useRef({ attempts: 0, timer: null });
  const aliveRef     = React.useRef(true);          // false once consumer unmounts
  const nickRef      = React.useRef(nick);          // latest assigned nick for outgoing /nick reapply
  React.useEffect(() => { nickRef.current = nick; }, [nick]);

  // append helper that keeps the buffer bounded
  const append = React.useCallback((frame) => {
    setMessages((prev) => {
      const next = prev.concat(frame);
      if (next.length > MSG_BUFFER_MAX) next.splice(0, next.length - MSG_BUFFER_MAX);
      return next;
    });
  }, []);

  const pushLocal = React.useCallback((text, severity) => {
    append({ t: 'system', id: localId(), text, ts: Date.now(), local: true, severity });
  }, [append]);

  const connect = React.useCallback(() => {
    if (!aliveRef.current) return;
    if (!url) { setStatus('offline'); pushLocal('IRC_URL not configured', 'err'); return; }

    let target;
    try {
      target = new URL(url);
      const stored = loadStoredNick();
      if (stored) target.searchParams.set('nick', stored);
    } catch {
      setStatus('offline');
      pushLocal(`bad IRC_URL: ${url}`, 'err');
      return;
    }

    let ws;
    try { ws = new WebSocket(target.toString()); }
    catch (e) {
      setStatus('reconnecting');
      scheduleReconnect();
      return;
    }
    wsRef.current = ws;
    setStatus((s) => (s === 'online' ? 'reconnecting' : 'connecting'));

    ws.addEventListener('open', () => {
      reconnectRef.current.attempts = 0;
      setStatus('online');
    });

    ws.addEventListener('message', (ev) => {
      let frame;
      try { frame = JSON.parse(ev.data); } catch { return; }
      if (!frame || typeof frame.t !== 'string') return;

      if (frame.t === 'hello') {
        const assigned = frame.nick || nickRef.current;
        setNick(assigned);
        saveStoredNick(assigned);
        if (Array.isArray(frame.presence)) setPresence(frame.presence);
        if (Array.isArray(frame.history)) {
          // Replace any prior session's tail with: a separator + server history.
          setMessages((prev) => {
            const sep = { t: 'system', id: localId(), text: `connected as ${assigned} to ${channel}`, ts: Date.now(), local: true };
            const tail = frame.history.slice(-MSG_BUFFER_MAX + 1);
            const next = prev.concat([sep], tail);
            if (next.length > MSG_BUFFER_MAX) next.splice(0, next.length - MSG_BUFFER_MAX);
            return next;
          });
        }
        return;
      }

      if (frame.t === 'join') {
        setPresence((p) => p.includes(frame.nick) ? p : [...p, frame.nick].sort());
        append(frame);
        return;
      }
      if (frame.t === 'part') {
        setPresence((p) => p.filter((n) => n !== frame.nick));
        append(frame);
        return;
      }
      if (frame.t === 'nick') {
        setPresence((p) => {
          const next = p.filter((n) => n !== frame.from);
          if (!next.includes(frame.to)) next.push(frame.to);
          next.sort();
          return next;
        });
        // If the rename was ours, update local state.
        if (frame.from === nickRef.current) {
          setNick(frame.to);
          saveStoredNick(frame.to);
        }
        append(frame);
        return;
      }
      if (frame.t === 'msg' || frame.t === 'action' || frame.t === 'system') {
        append(frame);
        return;
      }
    });

    const onCloseOrError = () => {
      if (!aliveRef.current) return;
      setStatus('reconnecting');
      scheduleReconnect();
    };
    ws.addEventListener('close', onCloseOrError);
    ws.addEventListener('error', onCloseOrError);
  }, [url, channel, append, pushLocal]);

  const scheduleReconnect = React.useCallback(() => {
    if (!aliveRef.current) return;
    const r = reconnectRef.current;
    if (r.timer) return;
    const delay = Math.min(RECONNECT_MAX_MS, RECONNECT_MIN_MS * Math.pow(2, r.attempts));
    r.attempts += 1;
    r.timer = setTimeout(() => {
      r.timer = null;
      connect();
    }, delay);
  }, [connect]);

  React.useEffect(() => {
    aliveRef.current = true;
    connect();
    return () => {
      aliveRef.current = false;
      if (reconnectRef.current.timer) {
        clearTimeout(reconnectRef.current.timer);
        reconnectRef.current.timer = null;
      }
      const ws = wsRef.current;
      wsRef.current = null;
      if (ws) try { ws.close(1000, 'unmount'); } catch {}
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // mount-only; reconnects are explicit via scheduleReconnect

  const sendFrame = React.useCallback((frame) => {
    const ws = wsRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN) {
      pushLocal('not connected — message dropped', 'warn');
      return false;
    }
    try { ws.send(JSON.stringify(frame)); return true; }
    catch { pushLocal('send failed', 'warn'); return false; }
  }, [pushLocal]);

  const send       = React.useCallback((text) => sendFrame({ t: 'msg', text }),       [sendFrame]);
  const sendAction = React.useCallback((text) => sendFrame({ t: 'action', text }),    [sendFrame]);
  const changeNick = React.useCallback((to)   => sendFrame({ t: 'nick', to }),        [sendFrame]);

  const disconnect = React.useCallback(() => {
    aliveRef.current = false;
    if (reconnectRef.current.timer) {
      clearTimeout(reconnectRef.current.timer);
      reconnectRef.current.timer = null;
    }
    const ws = wsRef.current;
    wsRef.current = null;
    if (ws) try { ws.close(1000, 'user quit'); } catch {}
    setStatus('offline');
  }, []);

  return {
    messages, presence, status, nick, channel,
    send, sendAction, setNick: changeNick, pushLocal, disconnect,
  };
}

// ── view ──────────────────────────────────────────────────────────────────
function formatTime(ts) {
  const d = new Date(ts);
  const hh = String(d.getHours()).padStart(2, '0');
  const mm = String(d.getMinutes()).padStart(2, '0');
  return `${hh}:${mm}`;
}

function IrcRow({ frame, selfNick }) {
  const time = formatTime(frame.ts);

  if (frame.t === 'msg') {
    const isSelf = frame.nick === selfNick;
    return (
      <div className={`bt-irc__row ${isSelf ? 'bt-irc__row--self' : ''}`}>
        <span className="bt-irc__time">{time}</span>
        <span className="bt-irc__nick">{frame.nick}</span>
        <span className="bt-irc__text">{frame.text}</span>
      </div>
    );
  }
  if (frame.t === 'action') {
    return (
      <div className="bt-irc__row bt-irc__row--action">
        <span className="bt-irc__time">{time}</span>
        <span className="bt-irc__action">* {frame.nick} {frame.text}</span>
      </div>
    );
  }
  if (frame.t === 'join') {
    return (
      <div className="bt-irc__row bt-irc__row--system">
        <span className="bt-irc__time">{time}</span>
        <span className="bt-irc__system">→ {frame.nick} joined</span>
      </div>
    );
  }
  if (frame.t === 'part') {
    return (
      <div className="bt-irc__row bt-irc__row--system">
        <span className="bt-irc__time">{time}</span>
        <span className="bt-irc__system">← {frame.nick} left</span>
      </div>
    );
  }
  if (frame.t === 'nick') {
    return (
      <div className="bt-irc__row bt-irc__row--system">
        <span className="bt-irc__time">{time}</span>
        <span className="bt-irc__system">* {frame.from} is now {frame.to}</span>
      </div>
    );
  }
  if (frame.t === 'system') {
    const sev = frame.severity ? ` bt-irc__row--${frame.severity}` : '';
    return (
      <div className={`bt-irc__row bt-irc__row--system${sev}`}>
        <span className="bt-irc__time">{time}</span>
        <span className="bt-irc__system">* {frame.text}</span>
      </div>
    );
  }
  return null;
}

function IrcView({ irc }) {
  return (
    <div className="bt-irc">
      <div className="bt-irc__head">
        <span className="bt-irc__channel">{irc.channel}</span>
        <span className="bt-irc__count">{irc.presence.length} online</span>
        <span className={`bt-irc__status bt-irc__status--${irc.status}`}>● {irc.status}</span>
      </div>
      <div className="bt-irc__log">
        {irc.messages.map((m, i) => (
          <IrcRow key={(m.id || `i${i}`) + ':' + i} frame={m} selfNick={irc.nick} />
        ))}
      </div>
    </div>
  );
}

window.useIrc  = useIrc;
window.IrcView = IrcView;
