import * as fabric from "fabric";
import { jsPDF } from "jspdf";
import { useCallback, useEffect, useRef, useState } from "react";
import {
  ArrowDownToLine,
  ArrowUpToLine,
  Cloud,
  Copy,
  Download,
  Eraser,
  FileDown,
  FlipHorizontal,
  FlipVertical,
  FolderOpen,
  Grid3x3,
  Group,
  HelpCircle,
  Maximize,
  MousePointer2,
  Redo2,
  RotateCw,
  Ruler,
  Save,
  Scissors,
  Trash2,
  Undo2,
  Ungroup,
  ZoomIn,
  ZoomOut,
  LogOut,
  Users,
  UserRound,
  MessageSquarePlus,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Slider } from "@/components/ui/slider";

import {
  CATEGORIES,
  LIBRARY,
  SNAP_STEPS,
  SYMMETRY_AXES,
  THREADS,
  threadMeters,
  type ElDef,
  type ThreadColor,
} from "@/lib/tatting-ru";
import { applyThread, applyWidth, buildElement, CUSTOM_PROPS, type TattingObject } from "@/lib/fabric-elements";
import { AUTOSAVE_KEY, newId, upsertProject, type StoredProject } from "@/lib/studio-storage";
import { currentUser, logout, roleLabel, type StoredUser } from "@/lib/auth-service";
import LoginDialog from "@/components/studio/LoginDialog";
import AdminPanel from "@/components/studio/AdminPanel";
import ProjectsDialog from "@/components/studio/ProjectsDialog";
import ProfileDialog from "@/components/studio/ProfileDialog";
import FeedbackDialog from "@/components/studio/FeedbackDialog";

const W = 1200;
const H = 850;

type Theme = "light" | "dark" | "grid";

const THEME_CFG: Record<Theme, { bg: string; line: string; bold: string; label: string; hint: string }> = {
  light: { bg: "#fdfaf3", line: "#e6dfd0", bold: "#d6cbb4", label: "Светлая", hint: "Светлая тема" },
  dark: { bg: "#14161b", line: "#2a2f3a", bold: "#3f4757", label: "Тёмная", hint: "Тёмная тема с контрастной сеткой" },
  grid: { bg: "#eef5fb", line: "#c3d9ee", bold: "#8fb6dc", label: "Миллиметровка", hint: "Чертёжная миллиметровка" },
};

export default function TattingStudio() {
  const elRef = useRef<HTMLCanvasElement | null>(null);
  const wrapRef = useRef<HTMLDivElement | null>(null);
  const fcRef = useRef<fabric.Canvas | null>(null);
  const history = useRef<{ stack: string[]; index: number; lock: boolean }>({ stack: [], index: -1, lock: false });
  const fileRef = useRef<HTMLInputElement | null>(null);
  const pinch = useRef<{ dist: number; x: number; y: number } | null>(null);
  const drag = useRef<{ x: number; y: number } | null>(null);
  const savedTimer = useRef<number | null>(null);

  const [unlocked, setUnlocked] = useState(true);
  const [theme, setTheme] = useState<Theme>("light");
  const [snap, setSnap] = useState(10);
  const [showGrid, setShowGrid] = useState(true);
  const [showRulers, setShowRulers] = useState(true);
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({ x: 0, y: 0 });
  const [panning, setPanning] = useState(false);
  const [booting, setBooting] = useState(true);
  const [symmetry, setSymmetry] = useState(1);
  const [thread, setThread] = useState<ThreadColor>(THREADS[0]!);
  const [width, setWidth] = useState(3);
  const [selected, setSelected] = useState<{ name: string; knots: number; thread: string } | null>(null);
  const [stats, setStats] = useState({ knots: 0, pathPx: 0, count: 0 });
  const [canUndo, setCanUndo] = useState(false);
  const [canRedo, setCanRedo] = useState(false);
  const [category, setCategory] = useState<string>(CATEGORIES[0]!);
  const [helpOpen, setHelpOpen] = useState(false);
  const [projectsOpen, setProjectsOpen] = useState(false);
  const [profileOpen, setProfileOpen] = useState(false);
  const [feedbackOpen, setFeedbackOpen] = useState(false);
  const [saved, setSaved] = useState(false);
  const [user, setUser] = useState<StoredUser | null>(null);
  const [adminOpen, setAdminOpen] = useState(false);

  useEffect(() => {
    const session = currentUser();
    setUser(session);
    setUnlocked(!!session && !session.temporary);
  }, []);

  const drawGrid = useCallback((canvas: fabric.Canvas, step: number, th: Theme, visible: boolean) => {
    const cfg = THEME_CFG[th];
    canvas.backgroundColor = cfg.bg;
    canvas.getObjects().forEach((o) => {
      if ((o as TattingObject).isGrid) canvas.remove(o);
    });
    if (visible) {
      const lines: fabric.FabricObject[] = [];
      const mk = (coords: [number, number, number, number], bold: boolean) => {
        const l = new fabric.Line(coords, {
          stroke: bold ? cfg.bold : cfg.line,
          strokeWidth: bold ? 1 : 0.5,
          selectable: false,
          evented: false,
        }) as unknown as TattingObject;
        l.isGrid = true;
        lines.push(l as unknown as fabric.FabricObject);
      };
      for (let x = 0; x <= W; x += step) mk([x, 0, x, H], x % (step * 5) === 0);
      for (let y = 0; y <= H; y += step) mk([0, y, W, y], y % (step * 5) === 0);
      lines.forEach((l) => {
        canvas.add(l);
        canvas.sendObjectToBack(l);
      });
    }
    canvas.requestRenderAll();
  }, []);

  const recalc = useCallback((canvas: fabric.Canvas) => {
    const objs = canvas.getObjects().filter((o) => !(o as TattingObject).isGrid);
    const knots = objs.reduce((s, o) => s + ((o as TattingObject).knots ?? 0), 0);
    const pathPx = objs.reduce((s, o) => s + ((o as TattingObject).pathPx ?? 0) * (o.scaleX ?? 1), 0);
    setStats({ knots, pathPx, count: objs.length });
  }, []);

  /** автосохранение текущей схемы в память браузера */
  const persist = useCallback((canvas: fabric.Canvas) => {
    try {
      const data = canvas.toObject(CUSTOM_PROPS) as { objects: { isGrid?: boolean }[] };
      data.objects = data.objects.filter((o) => !o.isGrid);
      localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(data));
      setSaved(true);
      if (savedTimer.current) window.clearTimeout(savedTimer.current);
      savedTimer.current = window.setTimeout(() => setSaved(false), 1400);
    } catch {
      /* приватный режим — просто пропускаем */
    }
  }, []);

  const snapshot = useCallback(
    (canvas: fabric.Canvas) => {
      if (history.current.lock) return;
      const json = JSON.stringify(canvas.toObject(CUSTOM_PROPS));
      const h = history.current;
      h.stack = h.stack.slice(0, h.index + 1);
      h.stack.push(json);
      if (h.stack.length > 40) h.stack.shift();
      h.index = h.stack.length - 1;
      setCanUndo(h.index > 0);
      setCanRedo(false);
      recalc(canvas);
      persist(canvas);
    },
    [recalc, persist],
  );

  // init
  useEffect(() => {
    if (!unlocked || !elRef.current) return;
    const canvas = new fabric.Canvas(elRef.current, {
      width: W,
      height: H,
      backgroundColor: THEME_CFG.light.bg,
      preserveObjectStacking: true,
    });
    fcRef.current = canvas;
    drawGrid(canvas, 10, "light", true);

    const onSelect = () => {
      const a = canvas.getActiveObject() as TattingObject | undefined;
      setSelected(
        a && a.elName
          ? { name: a.elName, knots: a.knots ?? 0, thread: a.threadName ?? "—" }
          : a
            ? { name: "Несколько элементов", knots: 0, thread: "—" }
            : null,
      );
    };
    canvas.on("selection:created", onSelect);
    canvas.on("selection:updated", onSelect);
    canvas.on("selection:cleared", () => setSelected(null));
    canvas.on("object:added", () => snapshot(canvas));
    canvas.on("object:modified", () => snapshot(canvas));
    canvas.on("object:removed", () => snapshot(canvas));

    const stored = typeof localStorage !== "undefined" ? localStorage.getItem(AUTOSAVE_KEY) : null;
    if (stored) {
      history.current.lock = true;
      void canvas
        .loadFromJSON(stored)
        .then(() => {
          drawGrid(canvas, 10, "light", true);
          canvas.requestRenderAll();
          history.current.lock = false;
          snapshot(canvas);
        })
        .catch(() => {
          history.current.lock = false;
        })
        .finally(() => window.setTimeout(() => setBooting(false), 220));
    } else {
      window.setTimeout(() => setBooting(false), 220);
    }


    return () => {
      void canvas.dispose();
      fcRef.current = null;
    };
  }, [unlocked, drawGrid, snapshot]);

  // привязка к сетке
  useEffect(() => {
    const canvas = fcRef.current;
    if (!canvas) return;
    const handler = (e: { target: fabric.FabricObject }) => {
      const t = e.target;
      t.set({ left: Math.round(t.left / snap) * snap, top: Math.round(t.top / snap) * snap });
    };
    canvas.on("object:moving", handler as never);
    return () => canvas.off("object:moving", handler as never);
  }, [snap]);

  // тема / шаг сетки
  useEffect(() => {
    const canvas = fcRef.current;
    if (!canvas) return;
    history.current.lock = true;
    drawGrid(canvas, snap, theme, showGrid);
    history.current.lock = false;
    document.documentElement.classList.toggle("dark", theme === "dark");
  }, [theme, snap, showGrid, drawGrid]);

  /** не даём уехать схеме за пределы окна: сцена всегда перекрывает вид */
  const clampVpt = useCallback((canvas: fabric.Canvas) => {
    const z = canvas.getZoom();
    const vpt = canvas.viewportTransform;
    const limX = Math.max(0, W * z - W);
    const limY = Math.max(0, H * z - H);
    vpt[4] = Math.min(W * 0.25, Math.max(-limX - W * 0.25, vpt[4]));
    vpt[5] = Math.min(H * 0.25, Math.max(-limY - H * 0.25, vpt[5]));
    canvas.setViewportTransform(vpt);
    setZoom(z);
    setPan({ x: vpt[4], y: vpt[5] });
  }, []);

  const applyZoom = useCallback(
    (next: number, px?: number, py?: number) => {
      const canvas = fcRef.current;
      if (!canvas) return;
      const z = Math.min(6, Math.max(0.3, next));
      canvas.zoomToPoint(new fabric.Point(px ?? W / 2, py ?? H / 2), z);
      clampVpt(canvas);
      canvas.requestRenderAll();
    },
    [clampVpt],
  );

  const panBy = useCallback(
    (dx: number, dy: number) => {
      const canvas = fcRef.current;
      if (!canvas) return;
      const vpt = canvas.viewportTransform;
      vpt[4] += dx;
      vpt[5] += dy;
      clampVpt(canvas);
      canvas.requestRenderAll();
    },
    [clampVpt],
  );

  const fitToScreen = useCallback(() => {
    const canvas = fcRef.current;
    if (!canvas) return;
    canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
    setZoom(1);
    setPan({ x: 0, y: 0 });
    canvas.requestRenderAll();
  }, []);

  // колесо мыши, панорама правой кнопкой и жесты касания
  useEffect(() => {
    const wrap = wrapRef.current;
    if (!wrap || !unlocked) return;
    const scale = () => {
      const r = wrap.getBoundingClientRect();
      return { kx: W / (r.width || 1), ky: H / (r.height || 1), r };
    };
    const toScene = (cx: number, cy: number) => {
      const { kx, ky, r } = scale();
      return { x: (cx - r.left) * kx, y: (cy - r.top) * ky };
    };

    const onWheel = (e: WheelEvent) => {
      const canvas = fcRef.current;
      if (!canvas) return;
      e.preventDefault();
      const dy = e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1);
      const p = toScene(e.clientX, e.clientY);
      applyZoom(canvas.getZoom() * Math.exp(-dy * 0.0015), p.x, p.y);
    };

    // правая кнопка мыши — перетаскивание холста
    const onDown = (e: PointerEvent) => {
      if (e.button !== 2 && e.button !== 1) return;
      e.preventDefault();
      e.stopPropagation();
      drag.current = { x: e.clientX, y: e.clientY };
      setPanning(true);
      wrap.setPointerCapture(e.pointerId);
    };
    const onPointerMove = (e: PointerEvent) => {
      if (!drag.current) return;
      e.preventDefault();
      const { kx, ky } = scale();
      panBy((e.clientX - drag.current.x) * kx, (e.clientY - drag.current.y) * ky);
      drag.current = { x: e.clientX, y: e.clientY };
    };
    const onUp = () => {
      drag.current = null;
      setPanning(false);
    };
    const onContext = (e: MouseEvent) => e.preventDefault();

    const dist = (t: TouchList) => Math.hypot(t[0]!.clientX - t[1]!.clientX, t[0]!.clientY - t[1]!.clientY);
    const mid = (t: TouchList) => ({
      x: (t[0]!.clientX + t[1]!.clientX) / 2,
      y: (t[0]!.clientY + t[1]!.clientY) / 2,
    });
    const onStart = (e: TouchEvent) => {
      if (e.touches.length === 2) {
        const m = mid(e.touches);
        const c = toScene(m.x, m.y);
        pinch.current = { dist: dist(e.touches), x: c.x, y: c.y };
        drag.current = { x: m.x, y: m.y };
      }
    };
    const onTouchMove = (e: TouchEvent) => {
      const canvas = fcRef.current;
      if (!canvas || e.touches.length !== 2 || !pinch.current || !drag.current) return;
      e.preventDefault();
      const d = dist(e.touches);
      const m = mid(e.touches);
      const c = toScene(m.x, m.y);
      const { kx, ky } = scale();
      applyZoom(canvas.getZoom() * (d / pinch.current.dist), c.x, c.y);
      panBy((m.x - drag.current.x) * kx, (m.y - drag.current.y) * ky);
      pinch.current = { dist: d, x: c.x, y: c.y };
      drag.current = { x: m.x, y: m.y };
    };
    const onEnd = () => {
      pinch.current = null;
      drag.current = null;
    };

    wrap.addEventListener("wheel", onWheel, { passive: false });
    wrap.addEventListener("pointerdown", onDown, true);
    wrap.addEventListener("pointermove", onPointerMove, true);
    wrap.addEventListener("pointerup", onUp, true);
    wrap.addEventListener("pointercancel", onUp, true);
    wrap.addEventListener("contextmenu", onContext);
    wrap.addEventListener("touchstart", onStart, { passive: false });
    wrap.addEventListener("touchmove", onTouchMove, { passive: false });
    wrap.addEventListener("touchend", onEnd);
    return () => {
      wrap.removeEventListener("wheel", onWheel);
      wrap.removeEventListener("pointerdown", onDown, true);
      wrap.removeEventListener("pointermove", onPointerMove, true);
      wrap.removeEventListener("pointerup", onUp, true);
      wrap.removeEventListener("pointercancel", onUp, true);
      wrap.removeEventListener("contextmenu", onContext);
      wrap.removeEventListener("touchstart", onStart);
      wrap.removeEventListener("touchmove", onTouchMove);
      wrap.removeEventListener("touchend", onEnd);
    };
  }, [unlocked, applyZoom, panBy]);


  const addElement = (def: ElDef) => {
    const canvas = fcRef.current;
    if (!canvas) return;
    const cx = W / 2;
    const cy = H / 2;
    const axes = def.kind === "text" ? 1 : symmetry;
    if (axes <= 1) {
      const obj = buildElement(def, thread, width);
      obj.set({ left: cx, top: cy });
      canvas.add(obj);
      canvas.setActiveObject(obj);
    } else {
      const dist = 160;
      for (let i = 0; i < axes; i++) {
        const a = (i / axes) * Math.PI * 2;
        const obj = buildElement(def, thread, width);
        obj.set({
          left: Math.round((cx + Math.cos(a) * dist) / snap) * snap,
          top: Math.round((cy + Math.sin(a) * dist) / snap) * snap,
          angle: (a * 180) / Math.PI + 90,
        });
        canvas.add(obj);
      }
    }
    canvas.requestRenderAll();
  };

  const withActive = (fn: (obj: TattingObject, canvas: fabric.Canvas) => void) => {
    const canvas = fcRef.current;
    const a = canvas?.getActiveObject() as TattingObject | undefined;
    if (!canvas || !a) return;
    fn(a, canvas);
    canvas.requestRenderAll();
  };

  const duplicate = () =>
    withActive(async (a, canvas) => {
      const clone = (await a.clone(CUSTOM_PROPS as never)) as TattingObject;
      clone.set({ left: a.left + snap * 3, top: a.top + snap * 3 });
      canvas.add(clone);
      canvas.setActiveObject(clone);
      canvas.requestRenderAll();
    });

  const groupSelected = () => {
    const canvas = fcRef.current;
    const a = canvas?.getActiveObject();
    if (!canvas || !a || a.type !== "activeselection") return;
    const sel = a as fabric.ActiveSelection;
    const g = new fabric.Group(sel.removeAll()) as TattingObject;
    g.elName = "Группа элементов";
    g.knots = 0;
    canvas.add(g);
    canvas.setActiveObject(g);
    canvas.requestRenderAll();
    snapshot(canvas);
  };

  const ungroupSelected = () => {
    const canvas = fcRef.current;
    const a = canvas?.getActiveObject() as fabric.Group | undefined;
    if (!canvas || !a || a.type !== "group") return;
    const items = a.removeAll();
    canvas.remove(a);
    items.forEach((o) => canvas.add(o));
    canvas.discardActiveObject();
    canvas.requestRenderAll();
    snapshot(canvas);
  };

  const restore = (dir: -1 | 1) => {
    const canvas = fcRef.current;
    const h = history.current;
    const next = h.index + dir;
    if (!canvas || next < 0 || next >= h.stack.length) return;
    h.index = next;
    h.lock = true;
    void canvas.loadFromJSON(h.stack[next]!).then(() => {
      drawGrid(canvas, snap, theme, showGrid);
      canvas.requestRenderAll();
      h.lock = false;
      setCanUndo(h.index > 0);
      setCanRedo(h.index < h.stack.length - 1);
      recalc(canvas);
    });
  };

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (!(e.ctrlKey || e.metaKey)) return;
      if (e.key.toLowerCase() === "z") {
        e.preventDefault();
        restore(e.shiftKey ? 1 : -1);
      }
      if (e.key.toLowerCase() === "y") {
        e.preventDefault();
        restore(1);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  });

  const exportPng = () => {
    const canvas = fcRef.current;
    if (!canvas) return;
    const url = canvas.toDataURL({ format: "png", multiplier: 3 });
    const a = document.createElement("a");
    a.href = url;
    a.download = "frivolite-uzor.png";
    a.click();
  };

  const exportPdf = () => {
    const canvas = fcRef.current;
    if (!canvas) return;
    const url = canvas.toDataURL({ format: "png", multiplier: 3 });
    const pdf = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" });
    const ratio = Math.min((297 - 20) / W, (210 - 24) / H);
    pdf.setFontSize(12);
    pdf.text("Shema frivolite / RealKnot Studio", 10, 12);
    pdf.addImage(url, "PNG", 10, 16, W * ratio, H * ratio);
    pdf.save("frivolite-uzor.pdf");
  };

  const currentData = () => {
    const canvas = fcRef.current;
    if (!canvas) return null;
    const data = canvas.toObject(CUSTOM_PROPS) as { objects: { isGrid?: boolean }[] };
    data.objects = data.objects.filter((o) => !o.isGrid);
    return data;
  };

  const saveProjectFile = () => {
    const data = currentData();
    if (!data) return;
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "frivolite-proekt.json";
    a.click();
    URL.revokeObjectURL(a.href);
  };

  const saveToGallery = () => {
    const canvas = fcRef.current;
    const data = currentData();
    if (!canvas || !data) return;
    const title = window.prompt("Название проекта", `Схема ${new Date().toLocaleDateString("ru-RU")}`);
    if (!title) return;
    if (!user) return;
    const project: StoredProject = {
      id: newId(),
      ownerId: user.id,
      title: title.trim(),
      updatedAt: Date.now(),
      elements: stats.count,
      thumb: canvas.toDataURL({ format: "png", multiplier: 0.25 }),
      data,
    };
    upsertProject(project);
    setProjectsOpen(false);
    window.setTimeout(() => setProjectsOpen(true), 120);
  };

  const loadData = (payload: string | object) => {
    const canvas = fcRef.current;
    if (!canvas) return;
    history.current.lock = true;
    void canvas.loadFromJSON(payload as never).then(() => {
      drawGrid(canvas, snap, theme, showGrid);
      canvas.requestRenderAll();
      history.current.lock = false;
      snapshot(canvas);
    });
  };

  const openFile = (file: File) => void file.text().then((t) => loadData(t));

  const deleteSelected = () =>
    withActive((_a, c) => {
      c.getActiveObjects().forEach((o) => c.remove(o));
      c.discardActiveObject();
      c.requestRenderAll();
    });

  const clearCanvas = () => {
    const canvas = fcRef.current;
    if (!canvas) return;
    if (!window.confirm("Очистить холст? Все элементы схемы будут удалены.")) return;
    canvas.discardActiveObject();
    canvas
      .getObjects()
      .filter((o) => !(o as TattingObject).isGrid)
      .forEach((o) => canvas.remove(o));
    canvas.requestRenderAll();
    snapshot(canvas);
  };

  const meters = threadMeters(stats.pathPx);

  if (!unlocked || !user)
    return (
      <LoginDialog
        onSignedIn={(signed) => {
          setUser(signed);
          setUnlocked(true);
        }}
      />
    );

  return (
    <div className="flex h-[100dvh] flex-col overflow-hidden bg-background font-sans text-foreground">
      <input
        ref={fileRef}
        type="file"
        accept="application/json"
        className="hidden"
        onChange={(e) => {
          const f = e.target.files?.[0];
          if (f) openFile(f);
          e.target.value = "";
        }}
      />

      <header className="flex shrink-0 items-center gap-2 border-b border-border bg-card px-3 py-2">
        <div className="min-w-0 flex-1">
          <h1 className="truncate font-display text-xl leading-none lg:text-2xl">RealKnot Studio</h1>
          <p className="truncate text-[11px] text-muted-foreground">
            Редактор схем плетения · сетка {snap} px · правая кнопка — перемещение, колесо — масштаб
          </p>
        </div>

        <div className="hidden rounded-lg border border-border p-0.5 md:flex">
          {(Object.keys(THEME_CFG) as Theme[]).map((t) => (
            <button
              key={t}
              onClick={() => setTheme(t)}
              title={THEME_CFG[t].hint}
              className={`h-8 rounded-md px-2 text-xs font-medium transition-colors ${
                theme === t ? "bg-primary text-primary-foreground" : "text-muted-foreground"
              }`}
            >
              {THEME_CFG[t].label}
            </button>
          ))}
        </div>

        <button
          onClick={() => setProfileOpen(true)}
          title={`Профиль: ${user.username} · ${roleLabel(user.role)}`}
          className="hidden h-9 items-center gap-2 rounded-lg border border-border bg-secondary px-2.5 text-xs font-medium transition-colors hover:border-accent lg:flex"
        >
          {user.avatar ? (
            <img src={user.avatar} alt="" className="h-6 w-6 rounded-full object-cover" />
          ) : (
            <UserRound className="h-4 w-4 text-accent" />
          )}
          {user.displayName?.trim() || user.username}
        </button>

        <div className="flex items-center gap-1">
          <Button
            variant="outline"
            size="icon"
            className="h-9 w-9 lg:hidden"
            title="Профиль и мои работы"
            onClick={() => setProfileOpen(true)}
          >
            <UserRound className="h-4 w-4" />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="h-9 w-9"
            title="Написать: чего не хватает"
            onClick={() => setFeedbackOpen(true)}
          >
            <MessageSquarePlus className="h-4 w-4" />
          </Button>
          {user.role === "admin" && (
            <Button
              variant="outline"
              size="icon"
              className="h-9 w-9"
              title="Панель админа: пользователи и пожелания"
              onClick={() => setAdminOpen(true)}
            >
              <Users className="h-4 w-4" />
            </Button>
          )}
          <Button variant="outline" size="icon" className="h-9 w-9" title="Помощь" onClick={() => setHelpOpen(true)}>
            <HelpCircle className="h-4 w-4" />
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="h-9 w-9"
            title="Мои проекты"
            onClick={() => setProjectsOpen(true)}
          >
            <FolderOpen className="h-4 w-4" />
          </Button>
          <Button variant="outline" size="icon" className="h-9 w-9" title="Скачать .json" onClick={saveProjectFile}>
            <Save className="h-4 w-4" />
          </Button>
          <Button variant="outline" size="icon" className="h-9 w-9" title="Экспорт в PDF A4" onClick={exportPdf}>
            <FileDown className="h-4 w-4" />
          </Button>
          <Button size="sm" className="h-9 gap-1.5" title="Экспорт изображения PNG" onClick={exportPng}>
            <Download className="h-4 w-4" /> PNG
          </Button>
          <Button
            variant="outline"
            size="icon"
            className="h-9 w-9"
            title="Выйти из аккаунта"
            onClick={() => {
              logout();
              setUser(null);
              setUnlocked(false);
            }}
          >
            <LogOut className="h-4 w-4" />
          </Button>
        </div>
      </header>

      <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2 lg:flex-row lg:overflow-hidden lg:gap-3 lg:p-3">
        {/* Библиотека */}
        <aside className="w-full shrink-0 overflow-y-auto rounded-xl border border-border bg-card p-3 lg:w-56 xl:w-64">
          <h2 className="font-display text-base">Библиотека элементов</h2>

          <div className="mt-2 flex flex-wrap gap-1">
            {CATEGORIES.map((c) => (
              <button
                key={c}
                onClick={() => setCategory(c)}
                title={`Раздел «${c}»`}
                className={`h-8 rounded-md border px-2 text-xs font-medium transition-colors ${
                  category === c ? "border-accent bg-secondary" : "border-border text-muted-foreground"
                }`}
              >
                {c}
              </button>
            ))}
          </div>
          <div className="mt-2 grid grid-cols-2 gap-1.5 lg:grid-cols-1">
            {LIBRARY.filter((e) => e.category === category).map((def) => (
              <button
                key={def.id}
                onClick={() => addElement(def)}
                title={`Добавить: ${def.name} · ${def.kind === "text" ? "подпись" : `${def.knots} узлов`}`}
                className="flex w-full items-center gap-2 rounded-lg border border-border bg-background p-1.5 text-left transition-colors hover:border-accent hover:bg-secondary active:scale-[0.98]"
              >
                <ElementIcon def={def} color={thread.value} />
                <span className="min-w-0">
                  <span className="block truncate text-xs font-medium">{def.name}</span>
                  <span className="block text-[10px] text-muted-foreground">
                    {def.kind === "text" ? "подпись" : `${def.knots} узлов`}
                  </span>
                </span>
              </button>
            ))}
          </div>
        </aside>

        {/* Холст */}
        <main className="flex min-h-0 min-w-0 flex-1 flex-col">

          <div className="flex shrink-0 flex-wrap items-center gap-1 rounded-xl border border-border bg-card p-1.5">
            <Button variant="outline" size="icon" className="h-9 w-9" title="Отменить (Ctrl+Z)" disabled={!canUndo} onClick={() => restore(-1)}>
              <Undo2 className="h-4 w-4" />
            </Button>
            <Button variant="outline" size="icon" className="h-9 w-9" title="Повторить (Ctrl+Shift+Z)" disabled={!canRedo} onClick={() => restore(1)}>
              <Redo2 className="h-4 w-4" />
            </Button>
            <Button variant="outline" size="icon" className="h-9 w-9" title="Дублировать выбранный элемент" onClick={duplicate}>
              <Copy className="h-4 w-4" />
            </Button>
            <Button variant="outline" size="icon" className="h-9 w-9" title="Сгруппировать выделенные элементы" onClick={groupSelected}>
              <Group className="h-4 w-4" />
            </Button>
            <Button variant="outline" size="icon" className="h-9 w-9" title="Разгруппировать элементы" onClick={ungroupSelected}>
              <Ungroup className="h-4 w-4" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              className="h-9 w-9"
              title="Отразить по горизонтали"
              onClick={() => withActive((a) => a.set("flipX", !a.flipX))}
            >
              <FlipHorizontal className="h-4 w-4" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              className="h-9 w-9"
              title="Отразить по вертикали"
              onClick={() => withActive((a) => a.set("flipY", !a.flipY))}
            >
              <FlipVertical className="h-4 w-4" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              className="h-9 w-9"
              title="На передний план"
              onClick={() => withActive((a, c) => c.bringObjectToFront(a))}
            >
              <ArrowUpToLine className="h-4 w-4" />
            </Button>
            <Button
              variant="outline"
              size="icon"
              className="h-9 w-9"
              title="На задний план"
              onClick={() =>
                withActive((a, c) => {
                  c.sendObjectToBack(a);
                  c.getObjects()
                    .filter((o) => (o as TattingObject).isGrid)
                    .forEach((g) => c.sendObjectToBack(g));
                })
              }
            >
              <ArrowDownToLine className="h-4 w-4" />
            </Button>

            <span className="mx-1 hidden h-6 w-px bg-border sm:block" />

            <Button variant="outline" size="icon" className="h-9 w-9" title="Приблизить" onClick={() => applyZoom(zoom * 1.2)}>
              <ZoomIn className="h-4 w-4" />
            </Button>
            <Button variant="outline" size="icon" className="h-9 w-9" title="Отдалить" onClick={() => applyZoom(zoom / 1.2)}>
              <ZoomOut className="h-4 w-4" />
            </Button>
            <Button variant="outline" size="sm" className="h-9 gap-1.5" title="Вписать схему в экран" onClick={fitToScreen}>
              <Maximize className="h-4 w-4" />
              <span className="tabular-nums">{Math.round(zoom * 100)}%</span>
            </Button>
            <Button
              variant={showGrid ? "default" : "outline"}
              size="icon"
              className="h-9 w-9"
              title="Показать или скрыть сетку"
              onClick={() => setShowGrid((v) => !v)}
            >
              <Grid3x3 className="h-4 w-4" />
            </Button>
            <Button
              variant={showRulers ? "default" : "outline"}
              size="icon"
              className="h-9 w-9"
              title="Показать или скрыть линейки"
              onClick={() => setShowRulers((v) => !v)}
            >
              <Ruler className="h-4 w-4" />
            </Button>
          </div>

          <div className="tatting-frame relative mt-2 min-h-0 flex-1 rounded-xl border border-border bg-card p-1.5 shadow-sm">
            <div className={`tatting-box ${showRulers ? "with-rulers" : ""}`}>
              {showRulers && <RulerBar axis="x" zoom={zoom} pan={pan.x} />}
              <div className="flex min-h-0 min-w-0 gap-1">
                {showRulers && <RulerBar axis="y" zoom={zoom} pan={pan.y} />}
                <div
                  ref={wrapRef}
                  className={`tatting-wrap relative min-w-0 flex-1 overflow-hidden rounded-lg ${
                    panning ? "cursor-grabbing" : ""
                  }`}
                >
                  <canvas ref={elRef} className="touch-none" />
                  {booting && (
                    <div className="absolute inset-0 animate-pulse backdrop-blur-md">
                      <div className="h-full w-full bg-gradient-to-br from-secondary/70 via-card/60 to-secondary/70" />
                    </div>
                  )}
                </div>
              </div>
            </div>

            {/* мягкий индикатор автосохранения в углу */}
            <div
              aria-live="polite"
              className={`pointer-events-none absolute bottom-3 right-3 flex items-center gap-1.5 rounded-full border border-border bg-card/90 px-2.5 py-1 text-[11px] text-muted-foreground shadow-sm transition-all duration-300 ${
                saved ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0"
              }`}
            >
              <Cloud className="h-3.5 w-3.5 animate-pulse text-accent" /> сохранено
            </div>
          </div>



        </main>

        {/* Свойства */}
        <aside className="w-full shrink-0 space-y-3 overflow-y-auto rounded-xl border border-border bg-card p-3 lg:w-60 xl:w-72">
          <div>
            <h2 className="font-display text-base">Сетка и симметрия</h2>
            <div className="mt-1 text-xs text-muted-foreground">Шаг привязки</div>
            <div className="mt-1 flex gap-1">
              {SNAP_STEPS.map((s) => (
                <button
                  key={s}
                  onClick={() => setSnap(s)}
                  title={`Привязка к сетке ${s} px`}
                  className={`h-8 flex-1 rounded-lg border text-xs font-medium ${
                    snap === s ? "border-accent bg-secondary" : "border-border text-muted-foreground"
                  }`}
                >
                  {s} px
                </button>
              ))}
            </div>
            <div className="mt-2 text-xs text-muted-foreground">Круговая симметрия (осей)</div>
            <div className="mt-1 flex gap-1">
              {SYMMETRY_AXES.map((s) => (
                <button
                  key={s}
                  onClick={() => setSymmetry(s)}
                  title={s === 1 ? "Без симметрии" : `Радиальная симметрия: ${s} осей`}
                  className={`h-8 flex-1 rounded-lg border text-xs font-medium ${
                    symmetry === s ? "border-accent bg-secondary" : "border-border text-muted-foreground"
                  }`}
                >
                  {s === 1 ? "нет" : s}
                </button>
              ))}
            </div>
          </div>

          <div className="border-t border-border pt-3">
            <h2 className="font-display text-base">Нити и цвет</h2>
            {(["Металлик", "Пастель", "Градиент"] as const).map((g) => (
              <div key={g} className="mt-2">
                <div className="text-[10px] uppercase tracking-wide text-muted-foreground">{g}</div>
                <div className="mt-1 flex flex-wrap gap-1.5">
                  {THREADS.filter((t) => t.group === g).map((t) => (
                    <button
                      key={t.name}
                      title={`Нить: ${t.name}`}
                      aria-label={t.name}
                      onClick={() => {
                        setThread(t);
                        withActive((a) => applyThread(a, t));
                        setSelected((s) => (s ? { ...s, thread: t.name } : s));
                      }}
                      style={{ background: t.to ? `linear-gradient(135deg, ${t.value}, ${t.to})` : t.value }}
                      className={`h-8 w-8 rounded-full border-2 transition-transform active:scale-90 ${
                        thread.name === t.name ? "scale-110 border-accent" : "border-border"
                      }`}
                    />
                  ))}
                </div>
              </div>
            ))}
            <div className="mt-3">
              <div className="mb-1.5 flex items-center justify-between text-xs">
                <span className="flex items-center gap-1.5">
                  <Scissors className="h-3.5 w-3.5" /> Толщина нити
                </span>
                <span className="tabular-nums text-muted-foreground">{width.toFixed(1)}</span>
              </div>
              <Slider
                value={[width]}
                min={1}
                max={10}
                step={0.5}
                onValueChange={([v]) => {
                  const nv = v ?? 3;
                  setWidth(nv);
                  withActive((a) => applyWidth(a, nv));
                }}
              />
            </div>
          </div>

          <div className="border-t border-border pt-3">
            <h2 className="font-display text-base">Выбранный элемент</h2>
            {selected ? (
              <dl className="mt-1.5 space-y-1 text-xs">
                <div className="flex justify-between gap-2">
                  <dt className="text-muted-foreground">Название</dt>
                  <dd className="text-right font-medium">{selected.name}</dd>
                </div>
                <div className="flex justify-between gap-2">
                  <dt className="text-muted-foreground">Узлов</dt>
                  <dd className="font-medium tabular-nums">{selected.knots}</dd>
                </div>
                <div className="flex justify-between gap-2">
                  <dt className="text-muted-foreground">Нить</dt>
                  <dd className="font-medium">{selected.thread}</dd>
                </div>
              </dl>
            ) : (
              <p className="mt-1.5 text-xs text-muted-foreground">
                Выберите элемент на холсте, чтобы изменить цвет, толщину и положение.
              </p>
            )}
            <Button
              variant="destructive"
              size="sm"
              className="mt-2 h-9 w-full gap-2"
              title="Удалить выбранный элемент"
              disabled={!selected}
              onClick={deleteSelected}
            >
              <Trash2 className="h-4 w-4" /> Удалить элемент
            </Button>
          </div>
        </aside>
      </div>

      {/* Нижняя строка состояния */}
      <footer className="flex shrink-0 flex-wrap items-center justify-between gap-2 border-t border-border bg-card px-3 py-1.5">
        <p className="text-xs tabular-nums">
          Узлов: <strong>{stats.knots}</strong> · расход <strong>≈ {meters.toFixed(2)} м</strong>{" "}
          <span className="text-muted-foreground">(запас 15%)</span>
        </p>

        <div className="flex items-center gap-2">
          <Button
            variant="destructive"
            size="sm"
            className="h-10 gap-2"
            title="Удалить выбранный элемент"
            disabled={!selected}
            onClick={deleteSelected}
          >
            <Trash2 className="h-4 w-4" /> Удалить
          </Button>
          <Button
            variant="outline"
            size="sm"
            className="h-10 gap-2 border-destructive/40 text-destructive"
            title="Очистить холст полностью"
            onClick={clearCanvas}
          >
            <Eraser className="h-4 w-4" /> Очистить холст
          </Button>
        </div>
      </footer>

      <ProjectsDialog
        open={projectsOpen}
        onOpenChange={setProjectsOpen}
        onOpenProject={(p) => {
          loadData(p.data as object);
          setProjectsOpen(false);
        }}
        onImportFile={() => fileRef.current?.click()}
        onSaveCurrent={saveToGallery}
        ownerId={user.id}
      />

      <AdminPanel open={adminOpen} onOpenChange={setAdminOpen} />

      <ProfileDialog
        open={profileOpen}
        onOpenChange={setProfileOpen}
        user={user}
        onUserChange={setUser}
        onOpenProject={(p) => {
          loadData(p.data as object);
          setProfileOpen(false);
        }}
      />

      <FeedbackDialog open={feedbackOpen} onOpenChange={setFeedbackOpen} user={user} />

      <Dialog open={helpOpen} onOpenChange={setHelpOpen}>
        <DialogContent className="max-w-lg">
          <DialogHeader>
            <DialogTitle className="font-display text-2xl">Как пользоваться студией</DialogTitle>
            <DialogDescription>Три коротких шага для создания схемы фриволите.</DialogDescription>
          </DialogHeader>
          <ol className="space-y-4 text-sm">
            <li className="flex gap-3">
              <MousePointer2 className="mt-0.5 h-6 w-6 shrink-0 text-primary" />
              <span>
                <strong className="block">1. Добавьте и перетащите</strong>
                Нажмите на элемент в «Библиотеке» — он появится на холсте. Включите круговую симметрию, чтобы сразу
                получить мандалу или салфетку.
              </span>
            </li>
            <li className="flex gap-3">
              <RotateCw className="mt-0.5 h-6 w-6 shrink-0 text-primary" />
              <span>
                <strong className="block">2. Настройте</strong>
                Меняйте цвет и толщину нити, группируйте элементы, отражайте и меняйте порядок слоёв. Ctrl+Z отменяет
                действие.
              </span>
            </li>
            <li className="flex gap-3">
              <Download className="mt-0.5 h-6 w-6 shrink-0 text-primary" />
              <span>
                <strong className="block">3. Сохраните</strong>
                «Мои проекты» хранит схемы в браузере, PNG и PDF A4 — для печати, «Скачать .json» — файл проекта.
              </span>
            </li>
          </ol>
        </DialogContent>
      </Dialog>
    </div>
  );
}

function RulerBar({ axis, zoom, pan }: { axis: "x" | "y"; zoom: number; pan: number }) {
  const total = axis === "x" ? W : H;
  const step = zoom >= 2.5 ? 25 : zoom >= 1.4 ? 50 : zoom >= 0.7 ? 100 : 200;
  const from = Math.floor(-pan / zoom / step) * step;
  const to = from + Math.ceil(total / zoom / step) * step + step;
  const ticks: number[] = [];
  for (let t = from; t <= to; t += step) ticks.push(t);
  const pos = (t: number) => ((t * zoom + pan) / total) * 100;

  if (axis === "x")
    return (
      <div className="relative mb-1 ml-7 h-5 shrink-0 overflow-hidden rounded-md bg-secondary text-[10px] text-muted-foreground">
        {ticks.map((t) => (
          <span
            key={t}
            className="absolute top-0 flex h-full items-center border-l border-border pl-1"
            style={{ left: `${pos(t)}%` }}
          >
            {t}
          </span>
        ))}
      </div>
    );

  return (
    <div className="relative w-6 shrink-0 overflow-hidden rounded-md bg-secondary text-[10px] text-muted-foreground">
      {ticks.map((t) => (
        <span
          key={t}
          className="absolute left-0 flex w-full justify-center border-t border-border pt-0.5"
          style={{ top: `${pos(t)}%` }}
        >
          {t}
        </span>
      ))}
    </div>
  );
}


function ElementIcon({ def, color }: { def: ElDef; color: string }) {
  const r = 30;
  const picotR = 5;
  const dots = Array.from({ length: def.picots }, (_, i) => {
    const a = (i / Math.max(def.picots, 1)) * Math.PI * 2 - Math.PI / 2;
    return { cx: Math.cos(a) * (r + 4), cy: Math.sin(a) * (r + 4) };
  });
  return (
    <svg viewBox="-45 -45 90 90" className="h-8 w-8 shrink-0" aria-hidden>
      <g fill="none" stroke={color} strokeWidth={3}>
        {def.kind === "ring" && (
          <>
            <circle r={r} />
            {dots.map((d, i) => (
              <circle key={i} cx={d.cx} cy={d.cy} r={picotR} />
            ))}
          </>
        )}
        {(def.kind === "arc" || def.kind === "arc-curved") && (
          <>
            <path d="M -34 12 A 36 36 0 0 1 34 12" />
            {Array.from({ length: def.picots }, (_, i) => {
              const t = (i + 1) / (def.picots + 1);
              const a = Math.PI - t * Math.PI;
              return <circle key={i} cx={Math.cos(a) * 40} cy={-Math.sin(a) * 40 + 12} r={4} />;
            })}
          </>
        )}
        {def.kind === "picot" && (
          <>
            <path d="M -26 8 Q 0 -34 26 8" />
            <circle cy={-14} r={7} />
          </>
        )}
        {def.kind === "bead" && <circle r={16} fill={color} />}
        {def.kind === "crystal" && <polygon points="0,-28 20,0 0,28 -20,0" fill={color} />}
        {def.kind === "text" && (
          <text x="0" y="10" textAnchor="middle" fontSize="34" fill={color} stroke="none">
            А1
          </text>
        )}
      </g>
    </svg>
  );
}
