<script lang="ts">
  import { onMount } from 'svelte';
  import Chat from './Chat.svelte';
  import { gateway } from '../lib/gateway';
  import { app, handleError, markChatRead } from '../lib/app.svelte';
  import { canSendChat, sentThisTurn } from '../lib/chatlimit';
  import type { ChatMessage, StateView } from '../lib/model';

  // The Chat tab body, hosted by CommsHub (which supplies the nav bar + tab bar). The
  // hub lays it out as a non-scrolling column, so the soft keyboard simply resizes the
  // viewport with the input pinned to the bottom. It loads the game state (for the
  // turn-based chat/nudge toggle) and the message list, and clears the unread while open.
  let { id }: { id: string } = $props();

  let view = $state<StateView | null>(null);
  let messages = $state<ChatMessage[]>([]);
  let busy = $state(false);
  let tick = $state(0);
  // The opponents the viewer has blocked: when every opponent is blocked the composer is hidden
  // (only the chat log remains). blockedIds are blocked humans (by account); blockedRobotSeats are
  // the seats of blocked disguised robots in this game (a robot block is per game+seat).
  let blockedIds = $state(new Set<string>());
  let blockedRobotSeats = $state(new Set<number>());

  const myId = $derived(app.session?.userId ?? '');
  const isMyTurn = $derived(
    !!view && (view.game.status === 'active' || view.game.status === 'open') && view.game.toMove === view.seat,
  );
  // At most one chat message per turn: "already wrote this turn" is derived from the
  // message list against the move-driven turn start (lastActivityUnix), so it resets when
  // the turn advances. canSend hides the field once the limit is reached (and always on
  // the opponent's turn).
  const canSend = $derived(canSendChat(isMyTurn, view ? sentThisTurn(messages, myId, view.game.lastActivityUnix) : 0));
  // The nudge is offered only on the opponent's turn of an active game (you hurry the player
  // who is to move). A finished game is read-only: neither chat nor nudge is offered.
  const canNudge = $derived(!!view && view.game.status === 'active' && view.game.toMove !== view.seat);
  // While the auto-match game still has no opponent, chat and nudge are both disabled.
  const waiting = $derived(!!view && view.game.status === 'open');
  // peerBlocked is true when every seated opponent is one the viewer has blocked: the whole
  // composer is then hidden (a blocked opponent cannot be messaged).
  const peerBlocked = $derived.by(() => {
    const v = view;
    if (!v) return false;
    const opponents = v.game.seats.filter((s) => s.seat !== v.seat && !!s.accountId);
    return (
      opponents.length > 0 &&
      opponents.every((s) => blockedIds.has(s.accountId) || blockedRobotSeats.has(s.seat))
    );
  });
  const nudgeCooldownSecs = 3600;
  // The nudge is one-per-hour-per-game and clears once the player chats (engagement); the
  // backend stays authoritative, so a move-based reset is left to it.
  const nudgeOnCooldown = $derived.by(() => {
    void tick;
    let lastNudge = 0;
    let lastChat = 0;
    for (const m of messages) {
      if (m.senderId !== myId) continue;
      if (m.kind === 'nudge') lastNudge = Math.max(lastNudge, m.createdAtUnix);
      else lastChat = Math.max(lastChat, m.createdAtUnix);
    }
    if (lastNudge === 0 || Date.now() / 1000 - lastNudge >= nudgeCooldownSecs) return false;
    return lastChat <= lastNudge;
  });

  async function refresh() {
    try {
      messages = await gateway.chatList(id);
      markChatRead(id);
    } catch {
      /* best-effort */
    }
  }
  // loadState (re)reads the game state that drives the chat/nudge toggle and the per-turn
  // limit. The initial load surfaces an error; live reloads are best-effort.
  async function loadState(initial = false) {
    try {
      view = await gateway.gameState(id, false);
    } catch (e) {
      if (initial) handleError(e);
    }
  }
  // loadBlocked refreshes the viewer's blocked set (best-effort); guests have none.
  async function loadBlocked() {
    if (app.profile?.isGuest) return;
    try {
      const bl = await gateway.blocksList();
      blockedIds = new Set(bl.blocked.map((b) => b.accountId));
      blockedRobotSeats = new Set(bl.robots.filter((r) => r.gameId === id).map((r) => r.seat));
    } catch {
      /* best-effort */
    }
  }
  onMount(async () => {
    await loadState(true);
    await refresh();
    void loadBlocked();
  });

  // Live: refresh the message list on a chat / nudge for this game, and reload the state on
  // a turn / game-state change so the chat/nudge toggle and the per-turn limit (keyed on the
  // turn start) follow the live game.
  $effect(() => {
    const e = app.lastEvent;
    if (!e) return;
    if ((e.kind === 'chat_message' && e.message.gameId === id) || (e.kind === 'nudge' && e.gameId === id)) {
      void refresh();
    }
    if (
      (e.kind === 'your_turn' || e.kind === 'opponent_moved' || e.kind === 'game_over' || e.kind === 'opponent_joined') &&
      e.gameId === id
    ) {
      void loadState();
    }
    // A block/unblock applied: re-derive whether the composer should be hidden.
    if (e.kind === 'notify' && (e.sub === 'user_blocked' || e.sub === 'user_unblocked')) {
      void loadBlocked();
    }
  });
  // Re-evaluate the nudge cooldown on a timer so the control re-enables on time.
  $effect(() => {
    const h = setInterval(() => (tick += 1), 20000);
    return () => clearInterval(h);
  });

  async function sendChat(text: string) {
    busy = true;
    try {
      messages = [...messages, await gateway.chatPost(id, text)];
    } catch (e) {
      handleError(e);
    } finally {
      busy = false;
    }
  }
  async function nudge() {
    busy = true;
    try {
      messages = [...messages, await gateway.nudge(id)];
    } catch (e) {
      handleError(e);
    } finally {
      busy = false;
    }
  }
</script>

<Chat
  {messages}
  {myId}
  {busy}
  myTurn={isMyTurn}
  {canSend}
  {canNudge}
  {waiting}
  {nudgeOnCooldown}
  vsAi={!!view && view.game.vsAi}
  blocked={peerBlocked}
  onsend={sendChat}
  onnudge={nudge}
/>
