import { useMemo, useState, type FormEvent } from 'react';
import type { AgentView, PreemptionVerdict, WatchTarget } from '@stream-control/shared';
import { DEFAULT_PRIORITY, priorityLabel } from '@stream-control/shared';
import { api } from '../api';
import { formatDateTime, formatDuration, formatRelative } from '../format';
import {
  PREVIEW_SIZES,
  loadPreviewSize,
  preemption,
  previewSrc,
  recordingContext,
  savePreviewSize,
  stateLabel,
  tagStyle,
  usedTags,
  type PreviewSize,
  type RecordingContext,
} from '../streamers';
import { StreamerDetail } from './StreamerDetail';
import { TagChip, TagManager } from './Tags';

interface Props {
  targets: WatchTarget[];
  agents: AgentView[];
  notify: (message: string, tone?: 'info' | 'error') => void;
  notificationsEnabled: boolean;
  onEnableNotifications: () => void;
}

const FILTERS = [
  { id: 'all', label: 'Tous' },
  { id: 'favorites', label: '★ Favoris' },
  { id: 'live', label: 'En direct' },
] as const;

type FilterId = (typeof FILTERS)[number]['id'];

export function StreamersPage({
  targets,
  agents,
  notify,
  notificationsEnabled,
  onEnableNotifications,
}: Props) {
  const [url, setUrl] = useState('');
  const [busy, setBusy] = useState(false);
  const [filter, setFilter] = useState<FilterId>('all');
  const [openId, setOpenId] = useState<string | null>(null);
  const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
  const [managingTags, setManagingTags] = useState(false);
  const [previewSize, setPreviewSize] = useState<PreviewSize>(loadPreviewSize);

  function changePreviewSize(size: PreviewSize) {
    setPreviewSize(size);
    // Mémorisé côté navigateur : c'est un confort d'affichage propre à l'écran
    // qu'on a sous les yeux, pas un réglage à imposer aux autres postes.
    savePreviewSize(size);
  }

  // Tirées des profils plutôt que du serveur : la liste arrive déjà entière par
  // le flux temps réel, donc toujours à jour, et une étiquette que personne ne
  // porte n'aurait de toute façon rien à filtrer.
  const tags = useMemo(() => usedTags(targets), [targets]);

  function toggleTag(id: string) {
    setSelectedTags((current) => {
      const next = new Set(current);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

  async function add(event: FormEvent) {
    event.preventDefault();
    if (!url.trim()) return;
    setBusy(true);
    try {
      const { target } = await api.addTarget(url.trim(), null);
      notify(`« ${target.username} » ajouté au suivi`);
      setUrl('');
    } catch (err) {
      notify(err instanceof Error ? err.message : 'Ajout impossible', 'error');
    } finally {
      setBusy(false);
    }
  }

  /**
   * En direct d'abord, puis show privé, puis le reste ; à statut égal les
   * favoris devant, puis la priorité. L'ordre du statut prime : un direct en
   * cours appelle une décision, un favori hors-ligne non.
   */
  const ordered = useMemo(
    () =>
      [...targets].sort((a, b) => {
        const rank = (target: WatchTarget) =>
          target.state === 'public' ? 0 : target.state === 'private' ? 1 : 2;
        return (
          rank(a) - rank(b) ||
          Number(b.favorite) - Number(a.favorite) ||
          b.priority - a.priority ||
          a.username.localeCompare(b.username)
        );
      }),
    [targets],
  );

  const visible = ordered.filter((target) => {
    // Les deux filtres se cumulent — « en direct » ET portant l'une des
    // étiquettes retenues. Entre étiquettes en revanche, c'est une union : en
    // cocher une seconde élargit la sélection, ce qui est le geste attendu
    // quand on empile des critères de même nature.
    if (selectedTags.size > 0 && !target.tags.some((tag) => selectedTags.has(tag.id))) {
      return false;
    }
    if (filter === 'favorites') return target.favorite;
    if (filter === 'live') return target.state === 'public';
    return true;
  });

  // La fiche ouverte doit refléter les mises à jour temps réel, et se refermer
  // d'elle-même si le profil disparaît de la liste.
  const openTarget = openId ? (targets.find((target) => target.id === openId) ?? null) : null;

  return (
    <div className="page">
      <form className="row streamer-add" onSubmit={add}>
        <input
          value={url}
          onChange={(event) => setUrl(event.target.value)}
          placeholder="https://fr.stripchat.com/pseudo — ou simplement le pseudo"
          aria-label="Lien du profil à suivre"
        />
        <button className="primary" type="submit" disabled={busy || !url.trim()}>
          Suivre
        </button>
        {!notificationsEnabled && (
          <button type="button" className="ghost" onClick={onEnableNotifications}>
            🔔 Activer les notifications
          </button>
        )}
      </form>

      {targets.length > 0 && (
        <div className="row filter-row">
          {FILTERS.map((entry) => (
            <button
              key={entry.id}
              className={filter === entry.id ? 'chip active' : 'chip'}
              onClick={() => setFilter(entry.id)}
            >
              {entry.label}
            </button>
          ))}
          <div className="spacer" />
          <span className="muted small">Aperçus</span>
          {PREVIEW_SIZES.map((size) => (
            <button
              key={size.id}
              className={previewSize === size.id ? 'chip active' : 'chip'}
              onClick={() => changePreviewSize(size.id)}
              title={
                size.id === 'off'
                  ? "N'afficher aucune image du flux — et ne pas la charger"
                  : `Aperçu ${size.label.toLowerCase()}`
              }
            >
              {size.label}
            </button>
          ))}
          <span className="muted small">{visible.length} profil(s)</span>
        </div>
      )}

      {targets.length > 0 && (
        <div className="row filter-row tag-filters">
          {tags.map((tag) => (
            <button
              key={tag.id}
              className={selectedTags.has(tag.id) ? 'tag tag-filter active' : 'tag tag-filter'}
              style={tagStyle(tag.name)}
              onClick={() => toggleTag(tag.id)}
              title={`Ne garder que les profils portant « ${tag.name} »`}
            >
              {tag.name}
            </button>
          ))}
          {tags.length === 0 && (
            <span className="muted small">
              Aucune étiquette — pose-en une depuis la fiche d'un streamer pour trier la liste
              d'ici.
            </span>
          )}
          {selectedTags.size > 0 && (
            <button className="link-button" onClick={() => setSelectedTags(new Set())}>
              tout afficher
            </button>
          )}
          <div className="spacer" />
          <button className="chip" onClick={() => setManagingTags(true)} title="Renommer, supprimer">
            ⚙ Étiquettes
          </button>
        </div>
      )}

      {targets.length === 0 ? (
        <div className="empty">
          <h2>Aucun streamer suivi</h2>
          <p className="muted">
            Colle le lien d'un profil ci-dessus : tu seras prévenu dès qu'il passe en direct,
            et tu pourras lancer l'enregistrement d'un clic.
          </p>
        </div>
      ) : visible.length === 0 ? (
        <p className="muted small">Aucun profil pour ce filtre.</p>
      ) : (
        <div className={`streamer-grid preview-${previewSize}`}>
          {visible.map((target) => (
            <StreamerCard
              key={target.id}
              target={target}
              targets={targets}
              agents={agents}
              notify={notify}
              previewSize={previewSize}
              onOpen={() => setOpenId(target.id)}
            />
          ))}
        </div>
      )}

      {openTarget && (
        <StreamerDetail
          target={openTarget}
          targets={targets}
          agents={agents}
          notify={notify}
          onClose={() => setOpenId(null)}
        />
      )}

      {managingTags && (
        <TagManager
          onClose={() => setManagingTags(false)}
          // Une étiquette renommée ou supprimée touche tous les profils : le
          // serveur les republie, et le filtre en cours peut désigner une
          // étiquette qui n'existe plus.
          onChanged={() => setSelectedTags(new Set())}
        />
      )}
    </div>
  );
}

/**
 * Vignette : identité, statut, et la seule action qui presse — lancer ou
 * arrêter la capture. Tout le reste (agent assigné, automatisme, historique)
 * vit dans la fiche détaillée, qu'un clic sur la vignette ouvre.
 */
function StreamerCard({
  target,
  targets,
  agents,
  notify,
  previewSize,
  onOpen,
}: {
  target: WatchTarget;
  targets: WatchTarget[];
  agents: AgentView[];
  notify: (message: string, tone?: 'info' | 'error') => void;
  previewSize: PreviewSize;
  onOpen: () => void;
}) {
  const [busy, setBusy] = useState(false);
  const [avatarBroken, setAvatarBroken] = useState(false);
  // La valeur en échec, pas un simple booléen : `preview` change de contenu à
  // chaque sonde (nouveau `snapshotAt`) sans changer d'identité de composant,
  // un booléen figé resterait donc bloqué après le premier accroc réseau.
  const [brokenPreview, setBrokenPreview] = useState<string | null>(null);

  const state = stateLabel(target);
  const agent = agents.find((candidate) => candidate.id === target.agentId) ?? null;
  const context = recordingContext(target, agent, targets);
  const verdict = preemption(target, context);
  const isLive = target.state === 'public';
  // Rien n'est calculé — ni chargé — quand les aperçus sont coupés : ce sont des
  // images distantes, autant ne pas les demander si personne ne les regarde.
  const preview = isLive && previewSize !== 'off' ? previewSrc(target) : null;
  const showPreview = preview !== null && preview !== brokenPreview;

  const liveFor = isLive && target.statusChangedAt ? Date.now() - target.statusChangedAt : null;
  const lastDuration =
    target.lastLiveStartedAt && target.lastLiveEndedAt
      ? target.lastLiveEndedAt - target.lastLiveStartedAt
      : null;

  async function run(action: () => Promise<unknown>, success?: string) {
    setBusy(true);
    try {
      await action();
      if (success) notify(success);
    } catch (err) {
      notify(err instanceof Error ? err.message : 'Action impossible', 'error');
    } finally {
      setBusy(false);
    }
  }

  /** Les commandes ne doivent pas ouvrir la fiche en même temps qu'elles s'exécutent. */
  const isolate = (handler: () => void) => (event: React.MouseEvent) => {
    event.stopPropagation();
    handler();
  };

  return (
    <article
      // La vignette entière est cliquable ; chaque commande arrête la propagation.
      className={`streamer tone-${context.self ? 'rec' : state.tone} clickable`}
      onClick={onOpen}
      onKeyDown={(event) => {
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault();
          onOpen();
        }
      }}
      role="button"
      tabIndex={0}
      title="Ouvrir la fiche"
    >
      {showPreview && (
        // Un `<img>`, pas un flux : le serveur n'a que l'API REST de la
        // plateforme, ce n'est pas l'agent qui pilote un navigateur ici. Le
        // clic n'est pas intercepté : même agrandie, l'image reste celle de
        // cette fiche, et l'ouvrir en cliquant dessus reste cohérent.
        <div className="streamer-preview">
          <img
            src={preview}
            alt=""
            loading="lazy"
            referrerPolicy="no-referrer"
            onError={() => setBrokenPreview(preview)}
          />
        </div>
      )}

      <header className="streamer-head">
        {target.avatarUrl && !avatarBroken ? (
          <img
            className="avatar"
            src={target.avatarUrl}
            alt=""
            loading="lazy"
            // Pas de fuite de l'URL du dashboard vers le CDN de la plateforme.
            referrerPolicy="no-referrer"
            onError={() => setAvatarBroken(true)}
          />
        ) : (
          <div className="avatar avatar-fallback" aria-hidden="true">
            {target.username.slice(0, 1).toUpperCase()}
          </div>
        )}

        <div className="streamer-identity">
          <span className="streamer-name">{target.label ?? target.username}</span>
          <span className="muted small">@{target.username}</span>
        </div>

        <button
          className={target.favorite ? 'icon star on' : 'icon star'}
          disabled={busy}
          title={target.favorite ? 'Retirer des favoris' : 'Mettre en favori'}
          onClick={isolate(() =>
            void run(() => api.updateTarget(target.id, { favorite: !target.favorite })),
          )}
        >
          {target.favorite ? '★' : '☆'}
        </button>

        <span className={`badge ${state.tone}`} title={`statut brut « ${target.rawStatus ?? '?'} »`}>
          {state.text}
        </span>
      </header>

      {/* Sans retrait possible ici : la vignette sert à voir, la fiche à régler. */}
      {target.tags.length > 0 && (
        <div className="tag-list">
          {target.tags.map((tag) => (
            <TagChip key={tag.id} name={tag.name} />
          ))}
        </div>
      )}

      <dl className="streamer-facts">
        <div>
          <dt>{isLive ? 'En direct depuis' : 'Statut depuis'}</dt>
          <dd>
            {isLive
              ? formatDuration(liveFor)
              : target.statusChangedAt
                ? formatRelative(target.statusChangedAt)
                : '—'}
          </dd>
        </div>
        <div>
          <dt>Dernier stream</dt>
          <dd>
            {isLive ? (
              <span className="muted">en cours</span>
            ) : target.lastLiveEndedAt ? (
              <>
                {formatDateTime(target.lastLiveEndedAt)}
                {lastDuration ? <span className="muted"> · {formatDuration(lastDuration)}</span> : null}
              </>
            ) : (
              <span className="muted">jamais observé</span>
            )}
          </dd>
        </div>
      </dl>

      <div className="actions" onClick={(event) => event.stopPropagation()}>
        {context.self ? (
          <button
            className="danger"
            disabled={busy || !agent?.online}
            onClick={() => void run(() => api.stopTarget(target.id), 'Enregistrement arrêté')}
          >
            ■ Arrêter
          </button>
        ) : (
          <button
            className="primary"
            disabled={busy || !agent?.online || !isLive || verdict?.allowed === false}
            title={recordTitle(target, agent, context, verdict)}
            onClick={() => void run(() => api.recordTarget(target.id), 'Enregistrement lancé')}
          >
            {verdict?.allowed ? '● Enregistrer ⚡' : '● Enregistrer'}
          </button>
        )}

        <div className="spacer" />

        <span className="muted small">
          {agent ? agent.name : 'aucun agent'}
          {/* Le rang par défaut resterait du bruit : il ne dit rien de plus que
              l'absence de réglage. */}
          {target.priority !== DEFAULT_PRIORITY && ` · ${priorityLabel(target.priority)}`}
          {target.preempt && <span title="Peut interrompre une capture de rang inférieur"> ⚡</span>}
        </span>
      </div>

      <footer className="card-foot muted small">
        <span className={context.self ? 'session-rec' : undefined}>
          {context.self
            ? '⏺ enregistrement en cours'
            : context.busyWith
              ? `VM occupée : ${context.busyWith}`
              : target.lastCheckedAt
                ? `sondé ${formatRelative(target.lastCheckedAt)}`
                : 'pas encore sondé'}
        </span>
        {target.notify ? <span title="Notifications activées">🔔</span> : <span title="Notifications désactivées">🔕</span>}
      </footer>
    </article>
  );
}

function recordTitle(
  target: WatchTarget,
  agent: AgentView | null,
  context: RecordingContext,
  verdict: PreemptionVerdict | null,
): string {
  if (!target.agentId) return 'Assigne un agent à ce streamer (clique la fiche)';
  if (!agent?.online) return 'Agent hors-ligne';
  if (verdict?.allowed) {
    return `Interrompt la capture de ${context.busyWith} sur ${agent.name} — priorité supérieure`;
  }
  if (verdict) return `${agent.name} déjà pris : ${verdict.reason}`;
  if (target.state !== 'public') return "Le streamer n'est pas en direct";
  return agent.browser.enabled
    ? "Ouvrir la page, passer en plein écran et lancer l'enregistrement"
    : 'Lancer OBS seul — le pilotage du navigateur est désactivé sur cet agent, ' +
        'la page ne sera pas ouverte';
}
