import { useState } from 'react';
import type {
  AgentAction,
  AgentView,
  BrowserSettings,
  PresetApplyResult,
  RecordingPreset,
  RecordingSettings,
  SessionImportResult,
  StreamState,
  WatchSettings,
  WatchTarget,
} from '@stream-control/shared';
import { RECORDING_ENCODERS, RECORDING_PRESETS, findRecordingPreset } from '@stream-control/shared';
import { api } from '../api';

interface Props {
  agent: AgentView;
  /** Streamers déjà suivis : ils alimentent la sélection ci-dessous. */
  targets: WatchTarget[];
  onClose: () => void;
  onCommand: (id: string, action: AgentAction, params?: Record<string, unknown>) => Promise<void>;
  notify: (message: string, tone?: 'info' | 'error') => void;
}

const STATE_HINTS: Record<StreamState, string> = {
  public: 'en direct',
  private: 'show privé',
  offline: 'hors-ligne',
  unknown: '',
};

const QUALITY_LABELS: Record<RecordingPreset['quality'], string> = {
  Small: 'qualité haute',
  HQ: 'qualité très haute',
  Lossless: 'sans perte',
};

/** Résume en une ligne ce que le preset impose réellement à OBS. */
function describePreset(preset: RecordingPreset): string {
  return [
    preset.height ? `${preset.height}p` : 'définition de la scène',
    `${preset.fps} fps`,
    QUALITY_LABELS[preset.quality],
    preset.format === 'mkv' ? 'MKV' : 'MP4',
    `audio ${preset.audioBitrateKbps} kb/s`,
  ].join(' · ');
}

function cpuGauge(cost: number): string {
  return '●'.repeat(cost) + '○'.repeat(4 - cost);
}

/** Compte rendu de l'agent : ce qu'OBS a accepté, refusé, et ce qui reste à faire. */
function PresetReport({ result }: { result: PresetApplyResult }) {
  return (
    <div className="preset-report">
      <p className="small">
        <strong>{result.presetLabel}</strong> appliqué · {result.video} ·{' '}
        {result.applied.length} paramètre(s) écrit(s)
      </p>
      {result.skipped.length > 0 && (
        <p className="small error">Refusé par OBS : {result.skipped.join(', ')}</p>
      )}
      {result.restartRequired && (
        <p className="small warn-text">
          L'encodeur a changé. OBS continuera d'utiliser le précédent jusqu'à son redémarrage.
        </p>
      )}
    </div>
  );
}

export function AgentSettings({ agent, targets, onClose, onCommand, notify }: Props) {
  const [name, setName] = useState(agent.name);
  const [host, setHost] = useState(agent.obs.host);
  const [port, setPort] = useState(String(agent.obs.port));
  const [password, setPassword] = useState(agent.obs.password);
  const [autoConnect, setAutoConnect] = useState(agent.autoConnectObs);
  const [notes, setNotes] = useState(agent.notes ?? '');
  const [directory, setDirectory] = useState(agent.status.recordDirectory ?? '');
  const [token, setToken] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [watch, setWatch] = useState<WatchSettings>(agent.watch);
  const [browser, setBrowser] = useState<BrowserSettings>(agent.browser);
  const [recording, setRecording] = useState<RecordingSettings>(agent.recording);
  const [presetResult, setPresetResult] = useState<PresetApplyResult | null>(null);
  const [applyingPreset, setApplyingPreset] = useState(false);
  const [importResult, setImportResult] = useState<SessionImportResult | null>(null);
  const [importingSession, setImportingSession] = useState(false);

  const preset = findRecordingPreset(recording.presetId) ?? RECORDING_PRESETS[0]!;
  const encoderInfo = RECORDING_ENCODERS[recording.encoder];

  // Saisie libre : soit l'opérateur la demande, soit le pseudo déjà configuré
  // ne fait pas partie des streamers suivis.
  const [manualUsername, setManualUsername] = useState(
    () => Boolean(agent.watch.username) && !targets.some((t) => t.username === agent.watch.username),
  );

  function patchBrowser(patch: Partial<BrowserSettings>) {
    setBrowser((current) => ({ ...current, ...patch }));
  }

  function patchWatch(patch: Partial<WatchSettings>) {
    setWatch((current) => ({ ...current, ...patch }));
  }

  function patchFullscreen(patch: Partial<WatchSettings['fullscreen']>) {
    setWatch((current) => ({ ...current, fullscreen: { ...current.fullscreen, ...patch } }));
  }

  /**
   * Applique le preset actuellement sélectionné, enregistré ou non : c'est un
   * essai, on veut voir le compte rendu avant de figer le choix.
   */
  async function applyPreset() {
    setApplyingPreset(true);
    setPresetResult(null);
    try {
      const { data } = await api.command(agent.id, 'preset.apply', {
        presetId: recording.presetId,
        encoder: recording.encoder,
      });
      setPresetResult(data as PresetApplyResult);
    } catch (err) {
      notify(err instanceof Error ? err.message : 'Application du preset impossible', 'error');
    } finally {
      setApplyingPreset(false);
    }
  }

  /**
   * Copie les cookies du Firefox personnel dans le profil piloté, pour que la
   * capture démarre déjà connectée. Ferme au passage l'instance pilotée si
   * elle tournait — la prochaine capture la relance avec les cookies neufs.
   */
  async function importSession() {
    setImportingSession(true);
    setImportResult(null);
    try {
      const { data } = await api.command(agent.id, 'browser.importSession');
      setImportResult(data as SessionImportResult);
      notify('Session importée');
    } catch (err) {
      notify(err instanceof Error ? err.message : "Import de session impossible", 'error');
    } finally {
      setImportingSession(false);
    }
  }

  async function save() {
    setBusy(true);
    try {
      await api.updateAgent(agent.id, {
        name,
        notes,
        autoConnectObs: autoConnect,
        obs: { host, port: Number(port), password },
        watch,
        browser,
        recording,
      } as Partial<AgentView>);
      notify('Configuration enregistrée');
      onClose();
    } catch (err) {
      notify(err instanceof Error ? err.message : 'Enregistrement impossible', 'error');
    } finally {
      setBusy(false);
    }
  }

  async function applyDirectory() {
    if (!directory.trim()) return;
    await onCommand(agent.id, 'recordDirectory.set', { directory: directory.trim() });
  }

  async function rotate() {
    if (!confirm("Régénérer le jeton ? L'agent sera déconnecté jusqu'à sa reconfiguration.")) return;
    try {
      const result = await api.rotateToken(agent.id);
      setToken(result.token);
    } catch (err) {
      notify(err instanceof Error ? err.message : 'Rotation impossible', 'error');
    }
  }

  async function remove() {
    if (!confirm(`Supprimer définitivement l'agent « ${agent.name} » ?`)) return;
    try {
      await api.deleteAgent(agent.id);
      notify('Agent supprimé');
      onClose();
    } catch (err) {
      notify(err instanceof Error ? err.message : 'Suppression impossible', 'error');
    }
  }

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(event) => event.stopPropagation()}>
        <header className="modal-head">
          <h2>Configuration · {agent.name}</h2>
          <button className="icon" onClick={onClose}>
            ✕
          </button>
        </header>

        <div className="modal-body">
          <label className="field">
            <span>Nom affiché</span>
            <input value={name} onChange={(event) => setName(event.target.value)} />
          </label>

          <div className="row">
            <label className="field grow">
              <span>Hôte obs-websocket</span>
              <input value={host} onChange={(event) => setHost(event.target.value)} />
            </label>
            <label className="field small-field">
              <span>Port</span>
              <input value={port} onChange={(event) => setPort(event.target.value)} inputMode="numeric" />
            </label>
          </div>

          <label className="field">
            <span>Mot de passe obs-websocket</span>
            <input
              type="password"
              value={password}
              onChange={(event) => setPassword(event.target.value)}
              placeholder="inchangé"
            />
          </label>

          <label className="checkbox">
            <input
              type="checkbox"
              checked={autoConnect}
              onChange={(event) => setAutoConnect(event.target.checked)}
            />
            <span>Connecter OBS automatiquement au démarrage de l'agent</span>
          </label>

          <label className="field">
            <span>Notes</span>
            <textarea rows={2} value={notes} onChange={(event) => setNotes(event.target.value)} />
          </label>

          <div className="row">
            <label className="field grow">
              <span>Dossier d'enregistrement OBS</span>
              <input
                value={directory}
                onChange={(event) => setDirectory(event.target.value)}
                placeholder="D:\\records ou /srv/records"
              />
            </label>
            <button className="ghost align-end" onClick={() => void applyDirectory()}>
              Appliquer
            </button>
          </div>

          <fieldset className="group">
            <legend>Preset d'enregistrement</legend>

            <label className="checkbox">
              <input
                type="checkbox"
                checked={recording.enabled}
                onChange={(event) =>
                  setRecording((current) => ({ ...current, enabled: event.target.checked }))
                }
              />
              <span>
                Piloter les réglages d'OBS depuis cette interface
                <span className="muted small">
                  {' '}
                  — décoché, l'agent ne touche à rien et OBS garde sa configuration manuelle.
                </span>
              </span>
            </label>

            <div className="row">
              <label className="field grow">
                <span>Preset</span>
                <select
                  value={recording.presetId}
                  onChange={(event) =>
                    setRecording((current) => ({ ...current, presetId: event.target.value }))
                  }
                >
                  {RECORDING_PRESETS.map((entry) => (
                    <option key={entry.id} value={entry.id}>
                      {entry.label} — CPU {cpuGauge(entry.cpuCost)}
                    </option>
                  ))}
                </select>
                <span className="muted small">{preset.summary}</span>
              </label>

              <label className="field grow">
                <span>Encodeur</span>
                <select
                  value={recording.encoder}
                  onChange={(event) =>
                    setRecording((current) => ({
                      ...current,
                      encoder: event.target.value as RecordingSettings['encoder'],
                    }))
                  }
                >
                  {Object.entries(RECORDING_ENCODERS).map(([id, info]) => (
                    <option key={id} value={id}>
                      {info.label}
                    </option>
                  ))}
                </select>
                <span className="muted small">{encoderInfo?.hint}</span>
              </label>
            </div>

            <p className="muted small">
              Réglages obtenus : <strong>{describePreset(preset)}</strong>
            </p>

            <div className="row">
              <button
                className="ghost"
                disabled={applyingPreset || !agent.online || agent.status.recording}
                title={
                  agent.status.recording
                    ? 'Enregistrement en cours — un changement de preset le corromprait'
                    : "Écrire ces réglages dans le profil OBS de cette VM"
                }
                onClick={() => void applyPreset()}
              >
                {applyingPreset ? 'Application…' : 'Appliquer maintenant'}
              </button>
              <span className="muted small align-end">
                Sinon appliqué à l'enregistrement de cette fiche.
              </span>
            </div>

            {presetResult && <PresetReport result={presetResult} />}

            <p className="muted small">
              La qualité prend effet au prochain démarrage d'enregistrement. Un changement
              d'encodeur, lui, exige un redémarrage d'OBS : celui-ci ne charge le sien qu'au
              lancement.
            </p>
          </fieldset>

          <fieldset className="group">
            <legend>Pilotage du navigateur</legend>

            <label className="checkbox">
              <input
                type="checkbox"
                checked={browser.enabled}
                onChange={(event) => patchBrowser({ enabled: event.target.checked })}
              />
              <span>
                Ouvrir la page du streamer et passer en plein écran automatiquement
              </span>
            </label>

            <label className="field">
              <span>Méthode de pilotage</span>
              <select
                value={browser.mode}
                onChange={(event) =>
                  patchBrowser({ mode: event.target.value as BrowserSettings['mode'] })
                }
              >
                <option value="bidi">WebDriver BiDi — l'agent pilote Firefox (recommandé)</option>
                <option value="launch">Lancement simple — une commande par page</option>
              </select>
              <span className="muted small">
                {browser.mode === 'bidi' ? (
                  <>
                    L'agent ouvre Firefox une fois et garde un canal de commande : il navigue,
                    déclenche le plein écran et <strong>vérifie</strong> qu'il a pris. Seule
                    méthode qui fonctionne en session Wayland — elle ne passe pas par le serveur
                    d'affichage.
                  </>
                ) : (
                  <>
                    Une commande est lancée par page, et le plein écran passe par xdotool : il
                    faut alors une session Xorg, et l'effet de la touche reste invérifiable.
                  </>
                )}
              </span>
            </label>

            {browser.mode === 'bidi' && (
              <p className="muted small">
                Au premier passage en BiDi, l'agent ouvre une <em>nouvelle</em> fenêtre Firefox,
                sur un profil qui lui est propre. Pointe la source « capture de fenêtre » d'OBS
                dessus une fois : ensuite elle reste ouverte, y compris entre deux
                enregistrements et lors d'un redémarrage de l'agent.
              </p>
            )}

            <div className="row">
              <label className="field grow">
                <span>Commande du navigateur</span>
                <input
                  value={browser.command}
                  onChange={(event) => patchBrowser({ command: event.target.value })}
                  placeholder="firefox"
                />
              </label>
              {browser.mode === 'launch' ? (
                <label className="field grow">
                  <span>Arguments (séparés par des espaces)</span>
                  <input
                    value={browser.args.join(' ')}
                    onChange={(event) =>
                      patchBrowser({ args: event.target.value.split(/\s+/).filter(Boolean) })
                    }
                    placeholder="aucun"
                  />
                  <span className="muted small">
                    Laisse vide : le lien part vers la fenêtre déjà ouverte.{' '}
                    <code>--new-window</code> en créerait une nouvelle à chaque capture, et OBS
                    perdrait sa cible.
                  </span>
                </label>
              ) : (
                <label className="field grow">
                  <span>Port de pilotage</span>
                  <input
                    value={String(browser.remotePort)}
                    onChange={(event) =>
                      patchBrowser({ remotePort: Number(event.target.value) || 9222 })
                    }
                    inputMode="numeric"
                  />
                  <span className="muted small">
                    Écoute uniquement sur 127.0.0.1, à ne changer que si le port est déjà pris.
                  </span>
                </label>
              )}
            </div>

            {browser.mode === 'bidi' && (
              <label className="field">
                <span>Profil Firefox dédié</span>
                <input
                  value={browser.profileDir}
                  onChange={(event) => patchBrowser({ profileDir: event.target.value })}
                  placeholder="laisse vide — l'agent choisit selon l'installation de Firefox"
                />
                <span className="muted small">
                  Un profil séparé est obligatoire : deux instances ne peuvent pas partager le
                  même, et pointer celui de l'opérateur redonnerait « Firefox est déjà ouvert ».
                  L'agent y écrit les réglages qui comptent pour un enregistrement — lecture
                  automatique autorisée, bandeau de plein écran supprimé, aucun onglet d'accueil.
                  Sur Ubuntu, où Firefox est un snap, le chemin ne doit contenir aucun segment
                  commençant par un point : un snap n'accède pas aux répertoires cachés.
                </span>
              </label>
            )}

            {browser.mode === 'bidi' && (
              <div className="row" style={{ alignItems: 'flex-start' }}>
                <div className="field grow">
                  <button
                    className="ghost"
                    disabled={importingSession}
                    onClick={() => void importSession()}
                  >
                    {importingSession ? 'Import…' : 'Importer les cookies du Firefox personnel'}
                  </button>
                  <span className="muted small">
                    Le profil piloté part vierge, donc déconnecté des sites où tu es connecté
                    ailleurs. Cette action copie les cookies de session (et rien d'autre) depuis
                    ton Firefox habituel, en lecture seule — rien n'y est jamais écrit. Connecte-toi
                    d'abord dans ce Firefox-là si ce n'est pas déjà fait. L'instance pilotée en
                    cours, s'il y en a une, est fermée : la prochaine capture repart connectée.
                  </span>
                  {importResult && (
                    <p className="small">
                      Copié depuis <code>{importResult.sourceProfile}</code> :{' '}
                      {importResult.copied.join(', ')}
                    </p>
                  )}
                </div>
              </div>
            )}

            <div className="row">
              <label className="field grow">
                <span>Délai avant le plein écran (s)</span>
                <input
                  value={String(Math.round(browser.readyDelayMs / 1000))}
                  onChange={(event) =>
                    patchBrowser({ readyDelayMs: (Number(event.target.value) || 0) * 1000 })
                  }
                  inputMode="numeric"
                />
                <span className="muted small">
                  Le temps que la page charge et que le lecteur démarre.
                </span>
              </label>
            </div>

            <label className="field">
              <span>À l'arrêt de l'enregistrement</span>
              <select
                value={browser.onStop}
                onChange={(event) =>
                  patchBrowser({ onStop: event.target.value as BrowserSettings['onStop'] })
                }
              >
                <option value="blank">Charger une page vide (recommandé)</option>
                <option value="keep">Ne rien faire</option>
                <option value="close">Fermer la fenêtre</option>
              </select>
              <span className="muted small">
                La page vide libère le décodeur vidéo tout en conservant la fenêtre — donc la
                source OBS. Fermer la fenêtre oblige à la repointer dans OBS au prochain
                enregistrement.
              </span>
            </label>

            <p className="muted small">
              {browser.mode === 'bidi' ? (
                <>
                  La touche du plein écran est celle configurée ci-dessous, dans « Surveillance du
                  stream ». Le titre de fenêtre, lui, ne sert plus : l'agent s'adresse à l'onglet,
                  pas à une fenêtre.
                </>
              ) : (
                <>
                  La touche et le titre de fenêtre utilisés pour le plein écran sont ceux
                  configurés ci-dessous, dans « Surveillance du stream ».
                </>
              )}
            </p>
          </fieldset>

          <fieldset className="group">
            <legend>Surveillance du stream</legend>

            <label className="checkbox">
              <input
                type="checkbox"
                checked={watch.enabled}
                onChange={(event) => patchWatch({ enabled: event.target.checked })}
              />
              <span>Mettre l'enregistrement en pause pendant les shows privés</span>
            </label>

            <div className="row">
              <label className="field small-field">
                <span>Plateforme</span>
                <select
                  value={watch.provider}
                  onChange={(event) =>
                    patchWatch({ provider: event.target.value as WatchSettings['provider'] })
                  }
                >
                  <option value="stripchat">Stripchat</option>
                </select>
              </label>
              <label className="field grow">
                <span>Streamer surveillé</span>
                {targets.length > 0 && !manualUsername ? (
                  <select
                    value={watch.username}
                    onChange={(event) => {
                      if (event.target.value === '__manual__') {
                        setManualUsername(true);
                        patchWatch({ username: '' });
                      } else {
                        patchWatch({ username: event.target.value });
                      }
                    }}
                  >
                    <option value="">— aucun —</option>
                    {targets.map((target) => {
                      const hint = STATE_HINTS[target.state];
                      return (
                        <option key={target.id} value={target.username}>
                          {target.label ?? target.username}
                          {hint ? ` — ${hint}` : ''}
                        </option>
                      );
                    })}
                    <option value="__manual__">Autre pseudo…</option>
                  </select>
                ) : (
                  <input
                    value={watch.username}
                    onChange={(event) => patchWatch({ username: event.target.value })}
                    placeholder="tel qu'il apparaît dans l'URL"
                    autoFocus={manualUsername}
                  />
                )}
                {targets.length === 0 ? (
                  <span className="muted small">
                    Aucun streamer suivi — ajoute-le dans l'onglet Streamers pour le
                    retrouver ici.
                  </span>
                ) : manualUsername ? (
                  <button
                    type="button"
                    className="link-button"
                    onClick={() => {
                      setManualUsername(false);
                      patchWatch({ username: '' });
                    }}
                  >
                    ← revenir à la liste
                  </button>
                ) : null}
              </label>
            </div>

            <div className="row">
              <label className="field grow">
                <span>Intervalle de sonde (s)</span>
                <input
                  value={String(Math.round(watch.pollIntervalMs / 1000))}
                  onChange={(event) =>
                    patchWatch({ pollIntervalMs: (Number(event.target.value) || 10) * 1000 })
                  }
                  inputMode="numeric"
                />
              </label>
              <label className="field grow">
                <span>Lectures avant pause</span>
                <input
                  value={String(watch.confirmations)}
                  onChange={(event) => patchWatch({ confirmations: Number(event.target.value) || 1 })}
                  inputMode="numeric"
                />
              </label>
            </div>

            <label className="field">
              <span>Statuts considérés comme « privé »</span>
              <input
                value={watch.privateStatuses.join(', ')}
                onChange={(event) =>
                  patchWatch({
                    privateStatuses: event.target.value
                      .split(',')
                      .map((entry) => entry.trim())
                      .filter(Boolean),
                  })
                }
              />
              <span className="muted small">
                Statuts renvoyés par Stripchat : public, private, p2p, groupShow,
                virtualPrivate, ticketShow, idle, off. Ceux listés ici mettent en pause sans
                clore. <strong>idle</strong> est le « revient bientôt » : le streamer est
                connecté mais ne diffuse pas. Retire groupShow pour continuer à enregistrer
                les shows de groupe.
              </span>
            </label>

            <label className="checkbox">
              <input
                type="checkbox"
                checked={watch.stopOnOffline}
                onChange={(event) => patchWatch({ stopOnOffline: event.target.checked })}
              />
              <span>Clore l'enregistrement quand le flux passe hors-ligne</span>
            </label>

            <label className="field">
              <span>Attente avant clôture (minutes)</span>
              <input
                value={String(Math.round(watch.offlineStopDelayMs / 60_000))}
                disabled={!watch.stopOnOffline}
                onChange={(event) =>
                  patchWatch({ offlineStopDelayMs: (Number(event.target.value) || 0) * 60_000 })
                }
                inputMode="numeric"
              />
              <span className="muted small">
                Le passage hors-ligne met l'enregistrement en pause immédiatement — l'écran
                d'attente n'entre jamais dans le fichier. Ce délai ne décide que de la clôture :
                en deçà, un retour du flux reprend le même fichier plutôt que d'en ouvrir un
                second.
              </span>
            </label>

            <label className="checkbox">
              <input
                type="checkbox"
                checked={watch.fullscreen.enabled}
                onChange={(event) => patchFullscreen({ enabled: event.target.checked })}
              />
              <span>Rappeler le plein écran à la fin du show privé</span>
            </label>

            <div className="row">
              <label className="field small-field">
                <span>Touche</span>
                <input
                  value={watch.fullscreen.key}
                  onChange={(event) => patchFullscreen({ key: event.target.value })}
                />
              </label>
              {browser.mode === 'launch' && (
                <label className="field grow">
                  <span>Titre de la fenêtre du lecteur</span>
                  <input
                    value={watch.fullscreen.windowMatch}
                    onChange={(event) => patchFullscreen({ windowMatch: event.target.value })}
                    placeholder="fragment du titre, ex. Stripchat"
                  />
                </label>
              )}
              <label className="field small-field">
                <span>Délai (s)</span>
                <input
                  value={String(Math.round(watch.fullscreen.delayMs / 1000))}
                  onChange={(event) =>
                    patchFullscreen({ delayMs: (Number(event.target.value) || 0) * 1000 })
                  }
                  inputMode="numeric"
                />
              </label>
            </div>

            <div className="row">
              <button className="ghost" onClick={() => void onCommand(agent.id, 'watch.check')}>
                Sonder maintenant
              </button>
              <button className="ghost" onClick={() => void onCommand(agent.id, 'hotkey.fullscreen')}>
                Tester le plein écran
              </button>
            </div>
          </fieldset>

          <div className="danger-zone">
            <div>
              <strong>Jeton d'agent</strong>
              <p className="muted small">
                Affiché une seule fois. À reporter dans le fichier <code>agent.config.json</code> de la VM.
              </p>
              {token && <code className="token">{token}</code>}
            </div>
            <button className="ghost" onClick={() => void rotate()}>
              Régénérer
            </button>
          </div>
        </div>

        <footer className="modal-foot">
          <button className="danger ghost" onClick={() => void remove()}>
            Supprimer l'agent
          </button>
          <div className="spacer" />
          <button className="ghost" onClick={onClose}>
            Annuler
          </button>
          <button className="primary" disabled={busy} onClick={() => void save()}>
            Enregistrer
          </button>
        </footer>
      </div>
    </div>
  );
}
