// web/components.jsx — общие UI-примитивы, совместимы со стилями из styles.css
// (это упрощённый аналог tweaks-panel.jsx + components.jsx из дизайна).

const Icon = ({ name, size = 14, stroke = 1.6, className = "ic", style }) => {
  const paths = ICONS[name];
  if (!paths) return null;
  return (
    <svg className={className} width={size} height={size} viewBox="0 0 24 24" fill="none"
         stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round"
         style={style} aria-hidden="true">
      {paths}
    </svg>
  );
};

const ICONS = {
  folder:   <><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></>,
  archive:  <><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></>,
  settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>,
  refresh:  <><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/></>,
  play:     <><polygon points="6 4 20 12 6 20 6 4"/></>,
  check:    <><polyline points="20 6 9 17 4 12"/></>,
  x:        <><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></>,
  link:     <><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></>,
  alert:    <><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></>,
  loader:   <><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></>,
  image:    <><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></>,
  film:     <><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/></>,
  layers:   <><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></>,
  edit:     <><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4z"/></>,
  chevronDown: <><polyline points="6 9 12 15 18 9"/></>,
  split:    <><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></>,
  merge:    <><path d="M8 6L8 18"/><path d="M16 6L16 18"/><polyline points="4 10 8 6 12 10"/><polyline points="12 14 16 18 20 14"/></>,
  inbox:    <><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></>,
  chevron:  <><polyline points="9 18 15 12 9 6"/></>,
  trash:    <><polyline points="3 6 5 6 21 6"/><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"/><path d="M10 11l0 6"/><path d="M14 11l0 6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></>,
  download: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></>,
  mic:      <><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></>,
};

// Spinner — раздаётся анимированно из CSS-keyframe ниже.
const Spinner = ({ size = 14 }) => (
  <Icon name="loader" size={size} className="ic spinning" />
);

// Toast — простой alert внизу страницы; берётся из window.toast(message, kind).
const Toast = ({ toasts }) => (
  <div style={{
    position: 'fixed', bottom: 20, right: 20, zIndex: 9999,
    display: 'flex', flexDirection: 'column', gap: 8,
  }}>
    {toasts.map((t, i) => (
      <div key={i} style={{
        padding: '10px 14px',
        background: t.kind === 'error' ? 'var(--warn-soft)' : 'var(--accent-soft)',
        color: t.kind === 'error' ? 'var(--warn)' : 'var(--accent-2)',
        border: '1px solid', borderColor: t.kind === 'error' ? 'var(--warn)' : 'var(--accent)',
        borderRadius: 'var(--r-lg)',
        fontSize: 13, maxWidth: 400,
      }}>
        {t.message}
      </div>
    ))}
  </div>
);

// Контекст тостов: один общий стейт + единственный рендер <Toast/> на корне.
// Раньше useToasts заводил локальный стейт в каждом экране — Toast в App не
// видел эти сообщения и они «уходили в воздух». Теперь useToasts возвращает
// push-функцию из контекста.
const ToastContext = React.createContext(null);

function ToastProvider({ children }) {
  const [toasts, setToasts] = React.useState([]);
  const show = React.useCallback((message, kind = 'info') => {
    const id = Date.now() + Math.random();
    setToasts((ts) => [...ts, { id, message, kind }]);
    setTimeout(() => setToasts((ts) => ts.filter((t) => t.id !== id)), 5000);
  }, []);
  return (
    <ToastContext.Provider value={show}>
      {children}
      <Toast toasts={toasts} />
    </ToastContext.Provider>
  );
}

// useToasts — возвращает [toasts, show]; toasts оставлен для совместимости
// с прежним API (всегда пустой — рендером занимается ToastProvider).
function useToasts() {
  const show = React.useContext(ToastContext);
  // Запасной вариант, если кто-то использовал хук вне Provider.
  if (!show) {
    return [[], (m) => console.warn("toast (no provider):", m)];
  }
  return [[], show];
}

// useAsync — обёртка над async-вызовом с loading/error/data состояниями.
function useAsync(fn, deps) {
  const [state, setState] = React.useState({ loading: true, error: null, data: null });
  const reload = React.useCallback(() => {
    // Держим старые данные во время рефетча — чтобы экран не мигал пустотой
    // при перезагрузке после сохранения правок (фаза 2, жалоба Лены на мигание).
    setState((s) => ({ loading: true, error: null, data: s.data }));
    fn()
      .then((data) => setState({ loading: false, error: null, data }))
      .catch((error) => setState({ loading: false, error, data: null }));
  }, deps);
  React.useEffect(() => { reload(); }, [reload]);
  return [state, reload];
}

// PlaceholderStub — компактная плашка «функция в работе» для пустых экранов.
function PlaceholderStub({ title, body, hint }) {
  return (
    <div style={{
      padding: "32px 28px", marginTop: 8,
      background: "var(--surface)",
      border: "1px dashed var(--line-2)",
      borderRadius: "var(--r-lg)",
      maxWidth: 720,
    }}>
      <div style={{ fontWeight: 500, fontSize: 15, color: "var(--ink)", marginBottom: 8 }}>
        {title}
      </div>
      <div style={{ fontSize: 13, color: "var(--ink-2)", lineHeight: 1.55, marginBottom: 12 }}>
        {body}
      </div>
      {hint && (
        <div style={{
          fontSize: 12, color: "var(--accent-2)",
          padding: "8px 12px",
          background: "var(--accent-soft)",
          borderRadius: "var(--r)",
          display: "inline-block",
        }}>
          <Icon name="alert" size={11} stroke={2} style={{ marginRight: 4, verticalAlign: "-1px" }}/>
          {hint}
        </div>
      )}
    </div>
  );
}

// MicButton — голосовой ввод через встроенный Web Speech API (Chrome/Edge),
// язык ru-RU. onInsert(text) вызывается на каждый ФИНАЛЬНЫЙ кусок речи —
// родитель сам дописывает его в поле (и сохраняет). Бесплатно, без ключей; в
// Chrome звук уходит на серверы Google. В Firefox API нет — кнопка прячется.
// Кнопка-тумблер: клик старт/стоп; пока слушает — оранжевая, над ней «живой»
// текст-превью того, что распознаётся (правка 2026-06-08).
function MicButton({ onInsert, size = 14, title }) {
  const SR = (typeof window !== "undefined") &&
    (window.SpeechRecognition || window.webkitSpeechRecognition);
  const [active, setActive] = React.useState(false);
  const [interim, setInterim] = React.useState("");
  const recRef = React.useRef(null);
  const [_, toast] = useToasts();

  React.useEffect(() => () => {
    if (recRef.current) { try { recRef.current.stop(); } catch (e) {} }
  }, []);

  if (!SR) return null; // браузер без поддержки (Firefox)

  const toggle = () => {
    if (active) {
      if (recRef.current) { try { recRef.current.stop(); } catch (e) {} }
      return;
    }
    let rec;
    try { rec = new SR(); } catch (e) { return; }
    rec.lang = "ru-RU";
    rec.continuous = true;
    rec.interimResults = true;
    rec.onresult = (e) => {
      let fin = "", inter = "";
      for (let i = e.resultIndex; i < e.results.length; i++) {
        const r = e.results[i];
        if (r.isFinal) fin += r[0].transcript;
        else inter += r[0].transcript;
      }
      if (fin.trim()) onInsert && onInsert(fin.trim());
      setInterim(inter);
    };
    rec.onerror = (e) => {
      const err = e && e.error;
      if (err === "not-allowed" || err === "service-not-allowed") {
        toast("Нет доступа к микрофону — разреши в браузере", "error");
      } else if (err && err !== "no-speech" && err !== "aborted") {
        toast("Распознавание речи: " + err, "warn");
      }
    };
    rec.onend = () => { setActive(false); setInterim(""); recRef.current = null; };
    recRef.current = rec;
    setActive(true);
    try { rec.start(); } catch (e) { setActive(false); recRef.current = null; }
  };

  return (
    <span style={{ position: "relative", display: "inline-flex" }}>
      <button type="button"
              onClick={toggle}
              onMouseDown={(e) => e.preventDefault()} /* не отбирать фокус у textarea */
              title={title || (active ? "Остановить запись" : "Говорить голосом (русский)")}
              style={{
                border: 0, cursor: "pointer", padding: 3, borderRadius: 6, lineHeight: 0,
                display: "inline-flex", alignItems: "center",
                background: active ? "var(--warn-soft)" : "transparent",
                color: active ? "var(--warn)" : "var(--ink-3)",
              }}>
        <Icon name="mic" size={size}/>
      </button>
      {active && (
        <span style={{
          position: "absolute", bottom: "100%", right: 0, marginBottom: 4,
          maxWidth: 280, padding: "4px 8px", background: "var(--surface)",
          border: "1px solid var(--warn)", borderRadius: 6,
          fontSize: 11, color: "var(--ink-2)", whiteSpace: "nowrap",
          overflow: "hidden", textOverflow: "ellipsis", zIndex: 50,
          boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
        }}>{interim || "слушаю…"}</span>
      )}
    </span>
  );
}

window.PlaceholderStub = PlaceholderStub;
window.MicButton = MicButton;
window.Icon = Icon;
window.Spinner = Spinner;
window.Toast = Toast;
window.ToastProvider = ToastProvider;
window.useToasts = useToasts;
window.useAsync = useAsync;
