<template>
  <div class="relative h-full min-h-0 bg-surface-0">
    <div ref="container" class="h-full w-full" />
    <div
      v-if="!controlling && !ended"
      class="pointer-events-none absolute top-2 right-4 rounded bg-zinc-800/90 px-2 py-0.5 text-xs text-amber-300"
    >
      {{ t('terminal.observer') }}
    </div>
    <div
      v-if="ended"
      class="absolute inset-x-0 top-0 bg-red-950/90 px-4 py-2 text-center text-sm text-red-200"
    >
      {{ t('terminal.sessionEnded') }}
    </div>
  </div>
</template>

<script setup lang="ts">
// Terminal xterm branché sur le client WS multiplexé : stdin, resize (seulement
// si controlling), flow control et resync entièrement délégués au ws-client.
import { onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';
import { wsClient, type Attachment } from '../lib/ws-client';
import { TERMINAL_THEME, TERMINAL_FONT_FAMILY } from '../lib/terminal-theme';

const props = withDefaults(defineProps<{ sessionId: string; mode?: 'interactive' | 'observer' }>(), {
  mode: 'interactive',
});

const { t } = useI18n();
const container = useTemplateRef<HTMLDivElement>('container');
const controlling = ref(true);
const ended = ref(false);

let term: Terminal | null = null;
let attachment: Attachment | null = null;
let resizeObserver: ResizeObserver | null = null;
let intersectionObserver: IntersectionObserver | null = null;
let onVisible: (() => void) | null = null;
let disposed = false;

onMounted(async () => {
  if (!container.value) return;

  term = new Terminal({
    cursorBlink: true,
    fontSize: 13,
    fontFamily: TERMINAL_FONT_FAMILY,
    scrollback: 20_000, // absorbe le replay élargi (REPLAY_TAIL_BYTES = 1 Mo) → on remonte plus loin
    theme: TERMINAL_THEME,
  });
  const fit = new FitAddon();
  term.loadAddon(fit);
  term.open(container.value);
  const activeTerm = term;

  try {
    const { WebglAddon } = await import('@xterm/addon-webgl');
    const webgl = new WebglAddon();
    // perte de contexte GPU : on dispose (retour au renderer DOM) PUIS on repeint, sinon l'écran
    // reste figé sur le dernier rendu WebGL jusqu'à la prochaine interaction (« texte qui ne s'affiche
    // que si ça bouge »).
    webgl.onContextLoss(() => {
      webgl.dispose();
      activeTerm.refresh(0, activeTerm.rows - 1);
    });
    term.loadAddon(webgl);
  } catch {
    // WebGL indisponible : xterm retombe sur le renderer DOM
  }

  fit.fit();

  // Re-mesure après stabilisation du layout flex : au `onMounted`, le conteneur peut ne pas avoir sa
  // taille finale (la hauteur flex se résout après le 1er paint) → un seul `fit()` donne trop peu de
  // lignes et déforme le TUI. On refait donc le calcul en `requestAnimationFrame`, puis on resynchronise
  // les dimensions PTY. Sert aussi après un resync (reconnexion) où l'écran est remis à plat.
  const refit = (): void => {
    try {
      fit.fit();
    } catch {
      /* conteneur sans dimension (démontage en cours) : ignoré */
    }
    attachment?.resize(activeTerm.cols, activeTerm.rows);
    // repaint explicite : le renderer WebGL ne repeint pas toujours une zone non « dirty » (retour
    // d'arrière-plan, resync) → on force un rafraîchissement pour éviter un écran figé.
    activeTerm.refresh(0, activeTerm.rows - 1);
  };

  try {
    attachment = await wsClient.attach({
      sessionId: props.sessionId,
      mode: props.mode,
      cols: activeTerm.cols,
      rows: activeTerm.rows,
      sink: {
        write: (data, callback) => activeTerm.write(data, callback),
        reset: () => {
          activeTerm.reset();
          requestAnimationFrame(refit);
        },
        onDetached: (reason) => {
          if (reason === 'session_exit' || reason === 'replaced') ended.value = true;
        },
        onControlChanged: (c) => {
          controlling.value = c;
        },
      },
    });
  } catch {
    // session introuvable ou déjà terminée
    ended.value = true;
    controlling.value = false;
    return;
  }
  if (disposed) {
    attachment.detach();
    return;
  }
  controlling.value = attachment.controlling;
  requestAnimationFrame(refit); // recale la taille une fois le layout fullbleed stabilisé

  activeTerm.onData((data) => attachment?.sendStdin(data));
  resizeObserver = new ResizeObserver(refit);
  resizeObserver.observe(container.value);
  // Re-révélation RÉELLE du conteneur : cellule de grille démontée/remontée (maximize↔grille), scroll
  // hors/dans le viewport, ou navigation SPA : autant de cas que `visibilitychange` (onglet only) ne
  // couvre PAS. Au retour visible, on repeint (le rendu était gelé hors-écran) ET on flushe l'ACK pour
  // sortir d'une éventuelle pause serveur (le callback de xterm.write était throttlé tant que caché).
  intersectionObserver = new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      // évite un fit() sur conteneur de taille 0 (remontage transitoire) → déforme le TUI
      if (entry.intersectionRect.width === 0 || entry.intersectionRect.height === 0) continue;
      refit();
      attachment?.flushAck();
    }
  });
  intersectionObserver.observe(container.value);
  // au retour en avant-plan, forcer un repaint (le renderer WebGL peut avoir « gelé » l'écran pendant
  // que l'onglet était caché) ; le ws-client flushe en parallèle les ACK pour relancer le flux.
  onVisible = (): void => {
    if (document.visibilityState === 'visible') activeTerm.refresh(0, activeTerm.rows - 1);
  };
  document.addEventListener('visibilitychange', onVisible);
  if (props.mode === 'interactive') activeTerm.focus();
});

onBeforeUnmount(() => {
  disposed = true;
  resizeObserver?.disconnect();
  intersectionObserver?.disconnect();
  if (onVisible) document.removeEventListener('visibilitychange', onVisible);
  attachment?.detach();
  term?.dispose();
});
</script>
