import { useState, useEffect, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { X, Loader2, Save, Beaker, Palette, Zap, Tag, Unlink } from 'lucide-react';
import { api, ApiError } from '../api/client';
import type { InventorySpool, SlicerSetting, SpoolCatalogEntry, LocalPreset, BuiltinFilament, SpoolmanBulkCreateResult, SpoolKProfileInput, SpoolmanFilamentEntry } from '../api/client';
import { Button } from './Button';
import { useToast } from '../contexts/ToastContext';
import type { SpoolFormData, PrinterWithCalibrations, ColorPreset } from './spool-form/types';
import { defaultFormData, validateForm, SPOOLMAN_LINKED_FIELDS } from './spool-form/types';
import { buildFilamentOptions, extractBrandsFromPresets, findPresetOption, loadRecentColors, parsePresetName, saveRecentColor } from './spool-form/utils';
import { MATERIALS } from './spool-form/constants';
import { FilamentSection } from './spool-form/FilamentSection';
import { ColorSection } from './spool-form/ColorSection';
import { AdditionalSection } from './spool-form/AdditionalSection';
import { SpoolmanFilamentPicker } from './spool-form/SpoolmanFilamentPicker';
import { PAProfileSection } from './spool-form/PAProfileSection';
import { SpoolUsageHistory } from './SpoolUsageHistory';

type TabId = 'filament' | 'pa-profile';

const CLEAR_TAG_PAYLOAD = { tag_uid: null, tray_uuid: null, tag_type: null, data_origin: null };

export type SpoolFormMode = 'create' | 'edit' | 'copy';

interface SpoolFormModalProps {
  isOpen: boolean;
  onClose: () => void;
  spool?: InventorySpool | null;
  mode: SpoolFormMode;
  printersWithCalibrations?: PrinterWithCalibrations[];
  currencySymbol: string;
  onSpoolsCreated?: (spools: InventorySpool[]) => void;
  /** When true, CRUD operations target the Spoolman inventory proxy endpoints. */
  spoolmanMode?: boolean;
  /** Query key to invalidate after mutations (differs for Spoolman vs local). */
  spoolsQueryKey?: string[];
}

export function SpoolFormModal({
  isOpen,
  onClose,
  spool,
  mode,
  printersWithCalibrations = [],
  currencySymbol,
  onSpoolsCreated,
  spoolmanMode = false,
  spoolsQueryKey = ['inventory-spools'],
}: SpoolFormModalProps) {
  const { t } = useTranslation();
  const queryClient = useQueryClient();
  const { showToast } = useToast();

  const isEditing = mode === 'edit';
  const isCopying = mode === 'copy';

  // Form state
  const [formData, setFormData] = useState<SpoolFormData>(defaultFormData);
  const [errors, setErrors] = useState<Partial<Record<keyof SpoolFormData, string>>>({});
  const [activeTab, setActiveTab] = useState<TabId>('filament');
  const [weightTouched, setWeightTouched] = useState(false);
  const [storageLocationTouched, setStorageLocationTouched] = useState(false);
  const [quickAdd, setQuickAdd] = useState(false);
  const [quantity, setQuantity] = useState(1);

  // Cloud presets
  const [cloudAuthenticated, setCloudAuthenticated] = useState(false);
  const [loadingCloudPresets, setLoadingCloudPresets] = useState(false);
  const [cloudPresets, setCloudPresets] = useState<SlicerSetting[]>([]);
  const [presetInputValue, setPresetInputValue] = useState('');

  // Spool catalog
  const [spoolCatalog, setSpoolCatalog] = useState<SpoolCatalogEntry[]>([]);

  // Local presets (OrcaSlicer imports)
  const [localPresets, setLocalPresets] = useState<LocalPreset[]>([]);

  // Built-in filaments (static fallback)
  const [builtinFilaments, setBuiltinFilaments] = useState<BuiltinFilament[]>([]);

  // Color catalog
  const [colorCatalog, setColorCatalog] = useState<{
    manufacturer: string;
    color_name: string;
    hex_color: string;
    material: string | null;
    // #1340: gradient + effect carried from the catalog entry through to the
    // color picker so they're applied alongside hex + name on selection.
    extra_colors?: string | null;
    effect_type?: string | null;
  }[]>([]);

  // Color state
  const [recentColors, setRecentColors] = useState<ColorPreset[]>([]);

  // PA Profile state
  const [fetchedCalibrations, setFetchedCalibrations] = useState<PrinterWithCalibrations[]>([]);
  const [selectedProfiles, setSelectedProfiles] = useState<Set<string>>(new Set());
  const [expandedPrinters, setExpandedPrinters] = useState<Set<string>>(new Set());

  // Use prop if provided, otherwise use self-fetched data
  const resolvedCalibrations = printersWithCalibrations.length > 0
    ? printersWithCalibrations
    : fetchedCalibrations;

  // Count selected PA profiles for tab badge
  const selectedProfileCount = selectedProfiles.size;

  // Fetch Spoolman filament catalog when in Spoolman mode
  // retry:false — Spoolman may be intentionally disabled (400); don't flood the server
  const { data: spoolmanFilaments = [], isLoading: isLoadingFilaments, error: filamentsError } = useQuery<SpoolmanFilamentEntry[], Error>({
    queryKey: ['spoolman-inventory-filaments'],
    queryFn: () => api.getSpoolmanInventoryFilaments(),
    enabled: spoolmanMode && isOpen,
    staleTime: 60_000,
    retry: false,
  });

  // Load recent colors on mount
  useEffect(() => {
    setRecentColors(loadRecentColors());
  }, []);

  // Fetch cloud presets and catalog when modal opens
  useEffect(() => {
    if (isOpen) {
      const fetchData = async () => {
        setLoadingCloudPresets(true);
        try {
          const status = await api.getCloudStatus();
          setCloudAuthenticated(status.is_authenticated);
          if (status.is_authenticated) {
            const presets = await api.getFilamentPresets();
            setCloudPresets(presets);
          }
        } catch (e) {
          console.error('Failed to fetch cloud presets:', e);
          setCloudAuthenticated(false);
        } finally {
          setLoadingCloudPresets(false);
        }
      };
      fetchData();
      if (!spoolmanMode) {
        api.getSpoolCatalog().then(setSpoolCatalog).catch(console.error);
      }
      api.getColorCatalog().then(setColorCatalog).catch(console.error);
      api.getLocalPresets().then(r => setLocalPresets(r.filament)).catch(console.error);
      api.getBuiltinFilaments().then(setBuiltinFilaments).catch(console.error);

      // Fetch printer calibrations if not provided via props
      if (printersWithCalibrations.length === 0) {
        (async () => {
          try {
            const printers = await api.getPrinters();
            const statuses = await Promise.all(
              printers.map(p => api.getPrinterStatus(p.id).catch(() => null)),
            );
            const results: PrinterWithCalibrations[] = [];
            for (let i = 0; i < printers.length; i++) {
              const printer = printers[i];
              const status = statuses[i];
              const connected = status?.connected ?? false;
              let calibrations: PrinterWithCalibrations['calibrations'] = [];
              if (connected) {
                try {
                  const kRes = await api.getKProfiles(printer.id);
                  calibrations = kRes.profiles.map(p => ({
                    cali_idx: p.slot_id,
                    filament_id: p.filament_id,
                    setting_id: p.setting_id || '',
                    name: p.name,
                    k_value: parseFloat(p.k_value) || 0,
                    n_coef: parseFloat(p.n_coef) || 0,
                    extruder_id: p.extruder_id,
                    nozzle_diameter: p.nozzle_diameter,
                  }));
                } catch {
                  // Printer may not support K-profiles
                }
              }
              results.push({ printer: { ...printer, connected }, calibrations });
            }
            setFetchedCalibrations(results);
          } catch (e) {
            console.error('Failed to fetch printer calibrations:', e);
          }
        })();
      }
    }
    // The effect intentionally depends only on `isOpen` (and the prop-side
    // calibration count) — re-running on every spoolmanMode toggle would
    // race the in-flight async fetches with unmount/teardown and emit
    // "test environment was torn down" errors in vitest. spoolmanMode only
    // gates a single fetch (getSpoolCatalog) which is cheap enough to skip
    // when the modal opens in Spoolman mode.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isOpen, printersWithCalibrations.length]);

  // Build filament options: cloud → local → fallback
  const filamentOptions = useMemo(
    () => buildFilamentOptions(cloudPresets, new Set(), localPresets, builtinFilaments),
    [cloudPresets, localPresets, builtinFilaments],
  );

  // Extract brands from presets
  const baseAvailableBrands = useMemo(() => {
    const presetBrands = extractBrandsFromPresets(cloudPresets, localPresets);
    const catalogBrands = colorCatalog
      .map(entry => entry.manufacturer?.trim())
      .filter((brand): brand is string => !!brand);
    const brandSet = new Set<string>([...presetBrands, ...catalogBrands]);
    return Array.from(brandSet).sort((a, b) => a.localeCompare(b));
  }, [cloudPresets, localPresets, colorCatalog]);

  const baseAvailableMaterials = useMemo(() => {
    const catalogMaterials = colorCatalog
      .map(entry => entry.material?.trim())
      .filter((material): material is string => !!material);
    const materialSet = new Set<string>([...MATERIALS, ...catalogMaterials]);
    return Array.from(materialSet).sort((a, b) => a.localeCompare(b));
  }, [colorCatalog]);

  const brandMaterialPairs = useMemo(() => {
    const pairs: Array<{ brand: string; material: string }> = [];

    for (const entry of colorCatalog) {
      const brand = entry.manufacturer?.trim();
      const material = entry.material?.trim();
      if (brand && material) pairs.push({ brand, material });
    }

    for (const preset of cloudPresets) {
      const parsed = parsePresetName(preset.name);
      if (parsed.brand && parsed.material) {
        pairs.push({ brand: parsed.brand, material: parsed.material });
      }
    }

    for (const preset of localPresets) {
      const parsed = parsePresetName(preset.name);
      const brand = preset.filament_vendor?.trim() || parsed.brand;
      const material = parsed.material;
      if (brand && material) {
        pairs.push({ brand, material });
      }
    }

    return pairs;
  }, [cloudPresets, colorCatalog, localPresets]);

  const brandToMaterials = useMemo(() => {
    const map = new Map<string, Set<string>>();
    for (const pair of brandMaterialPairs) {
      const brandKey = pair.brand.toLowerCase();
      const materialKey = pair.material.toLowerCase();
      if (!map.has(brandKey)) map.set(brandKey, new Set());
      map.get(brandKey)!.add(materialKey);
    }
    return map;
  }, [brandMaterialPairs]);

  const materialToBrands = useMemo(() => {
    const map = new Map<string, Set<string>>();
    for (const pair of brandMaterialPairs) {
      const brandKey = pair.brand.toLowerCase();
      const materialKey = pair.material.toLowerCase();
      if (!map.has(materialKey)) map.set(materialKey, new Set());
      map.get(materialKey)!.add(brandKey);
    }
    return map;
  }, [brandMaterialPairs]);

  const availableBrands = useMemo(() => {
    if (!formData.material) return baseAvailableBrands;
    const materialKey = formData.material.toLowerCase();
    const brandKeys = materialToBrands.get(materialKey);
    if (!brandKeys || brandKeys.size === 0) return baseAvailableBrands;
    return baseAvailableBrands.filter(brand => brandKeys.has(brand.toLowerCase()));
  }, [baseAvailableBrands, formData.material, materialToBrands]);

  const availableMaterials = useMemo(() => {
    if (!formData.brand) return baseAvailableMaterials;
    const brandKey = formData.brand.toLowerCase();
    const materialKeys = brandToMaterials.get(brandKey);
    if (!materialKeys || materialKeys.size === 0) return baseAvailableMaterials;
    return baseAvailableMaterials.filter(material => materialKeys.has(material.toLowerCase()));
  }, [baseAvailableMaterials, formData.brand, brandToMaterials]);

  // Find selected preset option
  const selectedPresetOption = useMemo(
    () => findPresetOption(formData.slicer_filament, filamentOptions),
    [formData.slicer_filament, filamentOptions],
  );

  // Reset form when modal opens/closes or spool changes
  useEffect(() => {
    if (isOpen) {
      if (spool) {
        // Legacy rows may carry a malformed rgba (e.g. the 7-char 'FFFFFFF'
        // from #1055 before the create/update pattern was enforced). The
        // backend SpoolUpdate schema rejects non-8-char hex on PATCH, so
        // re-submitting a malformed value would 422 every edit on that spool
        // — even edits that don't touch color. Normalize on load: any value
        // that isn't exactly 8 hex chars falls back to the default, so the
        // user can save unrelated fields (weight, material, note) without
        // first being forced to fix a color they may not even be aware is
        // broken. Saving also purges the bad value from the DB.
        const validRgba = spool.rgba && /^[0-9A-Fa-f]{8}$/.test(spool.rgba) ? spool.rgba : '808080FF';
        setFormData({
          material: spool.material || '',
          subtype: spool.subtype || '',
          brand: spool.brand || '',
          // #1319: leave color_name blank when the backend reports it was
          // synthesised from subtype — otherwise the form would round-trip
          // the synth value to Spoolman on save as if the user had set it,
          // which is what produced the "color reverts to subtype" symptom.
          color_name: spool.color_name_is_synthesized ? '' : (spool.color_name || ''),
          rgba: validRgba,
          extra_colors: spool.extra_colors || '',
          effect_type: spool.effect_type || '',
          label_weight: spool.label_weight || 1000,
          core_weight: spool.core_weight || 250,
          core_weight_catalog_id: spool.core_weight_catalog_id ?? null,
          weight_used: isCopying ? 0 : spool.weight_used || 0,
          slicer_filament: spool.slicer_filament || '',
          note: spool.note || '',
          cost_per_kg: spool.cost_per_kg ?? null,
          category: spool.category || '',
          low_stock_threshold_pct: spool.low_stock_threshold_pct ?? null,
          storage_location: spool.storage_location || '',
          spoolman_filament_id: null,
        });
        setPresetInputValue(spool.slicer_filament_name || spool.slicer_filament || '');

        // Load K-profiles for this spool
        if (spool.k_profiles && spool.k_profiles.length > 0) {
          const profileKeys = new Set<string>();
          for (const p of spool.k_profiles) {
            if (p.cali_idx !== null && p.cali_idx !== undefined) {
              profileKeys.add(`${p.printer_id}:${p.cali_idx}:${p.extruder ?? 'null'}`);
            }
          }
          setSelectedProfiles(profileKeys);
        } else {
          setSelectedProfiles(new Set());
        }
      } else {
        setFormData(defaultFormData);
        setPresetInputValue('');
        setSelectedProfiles(new Set());
        setQuickAdd(false);
        setQuantity(1);
      }
      setErrors({});
      setActiveTab('filament');
      setWeightTouched(false);
      setStorageLocationTouched(false);
    }
  }, [isOpen, spool, mode, isCopying]);

  // Expand all printers in PA profile section when calibrations are available
  useEffect(() => {
    if (isOpen && resolvedCalibrations.length > 0) {
      setExpandedPrinters(new Set(resolvedCalibrations.map(p => String(p.printer.id))));
    }
  }, [isOpen, resolvedCalibrations]);

  // Update field helper
  const updateField = <K extends keyof SpoolFormData>(key: K, value: SpoolFormData[K]) => {
    const isLinkedField = SPOOLMAN_LINKED_FIELDS.has(key);
    if (spoolmanMode && isLinkedField && formData.spoolman_filament_id !== null) {
      showToast(t('inventory.spoolmanFilamentUnlinked'), 'info');
    }
    setFormData(prev => ({
      ...prev,
      [key]: value,
      ...(spoolmanMode && isLinkedField && prev.spoolman_filament_id !== null
        ? { spoolman_filament_id: null }
        : {}),
    }));
    if (key === 'weight_used') setWeightTouched(true);
    if (key === 'storage_location') setStorageLocationTouched(true);
    if (errors[key]) {
      setErrors(prev => ({ ...prev, [key]: undefined }));
    }
  };

  // Prefill form from a Spoolman filament catalog entry
  // subtype extraction mirrors _spoolman_helpers.py logic
  const handleFilamentSelect = (filament: SpoolmanFilamentEntry) => {
    const material = filament.material || '';
    const name = filament.name || '';
    const subtype = material && name.startsWith(material) ? name.slice(material.length).trim() : name;
    const rawHex = (filament.color_hex ?? '').replace('#', '').toUpperCase();
    // Guard against short/malformed hex values — must be exactly 6 hex chars
    const colorHex = /^[0-9A-F]{6}$/.test(rawHex) ? rawHex : '808080';
    setFormData(prev => ({
      ...prev,
      spoolman_filament_id: filament.id,
      material,
      subtype,
      brand: filament.vendor?.name || '',
      rgba: `${colorHex}FF`,
      color_name: filament.color_name || '',
      label_weight: filament.weight ?? prev.label_weight,
    }));
    showToast(t('inventory.spoolmanFilamentSelected'), 'success');
  };

  // Handle color selection
  const handleColorUsed = (color: ColorPreset) => {
    setRecentColors(prev => saveRecentColor(color, prev));
  };

  // Mutations – dispatch to Spoolman proxy or local inventory based on mode
  const createMutation = useMutation({
    mutationFn: (data: Record<string, unknown>) =>
      spoolmanMode
        ? api.createSpoolmanInventorySpool(data as Parameters<typeof api.createSpoolmanInventorySpool>[0])
        : api.createSpool(data as Parameters<typeof api.createSpool>[0]),
    onSuccess: async (newSpool) => {
      if (newSpool?.id) {
        const ok = await saveKProfiles(newSpool.id);
        if (!ok) return;
      }
      await queryClient.invalidateQueries({ queryKey: spoolsQueryKey });
      if (onSpoolsCreated) onSpoolsCreated([newSpool]);
      showToast(t('inventory.spoolCreated'), 'success');
      onClose();
    },
    onError: (error: Error) => {
      if (error instanceof ApiError && error.status === 503) {
        showToast(t('inventory.spoolmanUnreachable'), 'error');
      } else {
        showToast(t('inventory.saveFailed'), 'error');
      }
    },
  });

  const bulkCreateMutation = useMutation<
    SpoolmanBulkCreateResult | InventorySpool[],
    Error,
    { data: Record<string, unknown>; qty: number }
  >({
    mutationFn: ({ data, qty }) =>
      spoolmanMode
        ? api.bulkCreateSpoolmanInventorySpools(data as Parameters<typeof api.bulkCreateSpoolmanInventorySpools>[0], qty)
        : api.bulkCreateSpools(data as Parameters<typeof api.bulkCreateSpools>[0], qty),
    onSuccess: async (result) => {
      // Spoolman bulk-create returns SpoolmanBulkCreateResult (207); local returns InventorySpool[].
      // Cast via unknown to satisfy strict TypeScript — the runtime shape is guaranteed by
      // the duck-type check ('created' in result) before any property access.
      const spoolmanResult = (spoolmanMode && 'created' in result)
        ? (result as unknown as SpoolmanBulkCreateResult)
        : null;
      const createdSpools: InventorySpool[] = spoolmanResult
        ? spoolmanResult.created
        : (result as InventorySpool[]);

      if (selectedProfiles.size > 0) {
        for (const s of createdSpools) {
          await saveKProfiles(s.id);
        }
      }
      await queryClient.invalidateQueries({ queryKey: spoolsQueryKey });
      if (onSpoolsCreated) onSpoolsCreated(createdSpools);
      if (spoolmanResult && spoolmanResult.failed_count > 0) {
        showToast(
          t('inventory.spoolsPartiallyCreated', {
            created: createdSpools.length,
            total: spoolmanResult.requested_count,
          }),
          'warning',
        );
      } else {
        showToast(t('inventory.spoolsCreated', { count: createdSpools.length }), 'success');
      }
      onClose();
    },
    onError: (error: Error) => {
      if (error instanceof ApiError && error.status === 503) {
        showToast(t('inventory.spoolmanUnreachable'), 'error');
      } else {
        showToast(t('inventory.saveFailed'), 'error');
      }
    },
  });

  const updateMutation = useMutation({
    mutationFn: (data: Record<string, unknown>) =>
      spoolmanMode
        ? api.updateSpoolmanInventorySpool(spool!.id, data as Parameters<typeof api.updateSpoolmanInventorySpool>[1])
        : api.updateSpool(spool!.id, data as Parameters<typeof api.updateSpool>[1]),
    onSuccess: async () => {
      if (spool?.id) {
        const ok = await saveKProfiles(spool.id);
        if (!ok) return;
      }
      await queryClient.invalidateQueries({ queryKey: spoolsQueryKey });
      showToast(t('inventory.spoolUpdated'), 'success');
      onClose();
    },
    onError: (error: Error) => {
      if (error instanceof ApiError && error.status === 503) {
        showToast(t('inventory.spoolmanUnreachable'), 'error');
      } else {
        showToast(t('inventory.saveFailed'), 'error');
      }
    },
  });

  const deleteTagMutation = useMutation({
    mutationFn: () => {
      if (spoolmanMode) {
        return api.updateSpoolmanInventorySpool(spool!.id, CLEAR_TAG_PAYLOAD as Parameters<typeof api.updateSpoolmanInventorySpool>[1]);
      }
      return api.updateSpool(spool!.id, CLEAR_TAG_PAYLOAD as Parameters<typeof api.updateSpool>[1]);
    },
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: spoolsQueryKey });
      showToast(t('inventory.rfidCleared', 'RFID tag cleared'), 'success');
      onClose();
    },
    onError: (error: Error) => {
      if (error instanceof ApiError && error.status === 503) {
        showToast(t('inventory.spoolmanUnreachable'), 'error');
      } else {
        showToast(t('inventory.tagClearFailed'), 'error');
      }
    },
  });

  // Fetch assignment for this spool (to show Unassign button). In Spoolman mode
  // the slot assignment lives in the spoolman_slot_assignments table keyed by
  // spoolman_spool_id, not in the legacy spool_assignments table — #1336 was the
  // resulting "Unassign button is always disabled" report.
  const { data: assignments } = useQuery({
    queryKey: ['spool-assignments'],
    queryFn: () => api.getAssignments(),
    enabled: isOpen && isEditing && !spoolmanMode,
  });
  const { data: spoolmanSlotAssignments } = useQuery({
    queryKey: ['spoolman-slot-assignments-all'],
    queryFn: () => api.getSpoolmanSlotAssignments(),
    enabled: isOpen && isEditing && spoolmanMode,
  });
  const spoolAssignment = (() => {
    if (!spool) return undefined;
    if (spoolmanMode) {
      return spoolmanSlotAssignments?.find(a => a.spoolman_spool_id === spool.id);
    }
    return assignments?.find(a => a.spool_id === spool.id);
  })();

  // Read inventory + settings caches (already populated by InventoryPage) to
  // drive the category autocomplete and low-stock-threshold placeholder. #729
  const { data: allSpools } = useQuery({
    queryKey: ['inventory-spools'],
    queryFn: () => api.getSpools(true),
    enabled: isOpen,
  });
  const { data: settingsForForm } = useQuery({
    queryKey: ['settings'],
    queryFn: api.getSettings,
    enabled: isOpen,
  });
  const availableCategories = (() => {
    const set = new Set<string>();
    for (const s of allSpools ?? []) {
      const c = s.category?.trim();
      if (c) set.add(c);
    }
    return Array.from(set).sort((a, b) => a.localeCompare(b));
  })();
  const globalLowStockThreshold = settingsForForm?.low_stock_threshold ?? 20;

  const unassignMutation = useMutation({
    mutationFn: async () => {
      if (!spoolAssignment) throw new Error('No assignment');
      if (spoolmanMode) {
        if (!spool) throw new Error('No spool');
        await api.unassignSpoolmanSlot(spool.id);
        return;
      }
      await api.unassignSpool(spoolAssignment.printer_id, spoolAssignment.ams_id, spoolAssignment.tray_id);
    },
    onSuccess: async () => {
      if (spoolmanMode) {
        await queryClient.invalidateQueries({ queryKey: ['spoolman-slot-assignments-all'] });
        await queryClient.invalidateQueries({ queryKey: ['spoolman-slot-assignments'] });
      } else {
        await queryClient.invalidateQueries({ queryKey: ['spool-assignments'] });
      }
      showToast(t('inventory.unassignSuccess', 'Spool unassigned'), 'success');
      onClose();
    },
    onError: (error: Error) => {
      showToast(error.message, 'error');
    },
  });

  // Save K-profiles for selected calibrations. Returns false if any error occurred.
  const saveKProfiles = async (spoolId: number): Promise<boolean> => {
    const saveApi = spoolmanMode ? api.saveSpoolmanKProfiles : api.saveSpoolKProfiles;

    if (selectedProfiles.size === 0) {
      try {
        await saveApi(spoolId, []);
        return true;
      } catch (e) {
        console.error('Failed to save K-profiles:', e);
        showToast(t('inventory.kProfileSaveFailed'), 'warning');
        return false;
      }
    }

    const profiles: SpoolKProfileInput[] = [];
    let dropped = 0;
    for (const key of selectedProfiles) {
      const [printerIdStr, caliIdxStr, extruderStr] = key.split(':');
      const printerId = parseInt(printerIdStr);
      const caliIdx = parseInt(caliIdxStr);
      const extruder = extruderStr === 'null' ? 0 : parseInt(extruderStr);

      const pc = resolvedCalibrations.find(p => p.printer.id === printerId);
      if (pc) {
        const cal = pc.calibrations.find(c => c.cali_idx === caliIdx);
        if (cal) {
          profiles.push({
            printer_id: printerId,
            extruder,
            nozzle_diameter: cal.nozzle_diameter || '0.4',
            k_value: cal.k_value,
            name: cal.name || null,
            cali_idx: cal.cali_idx,
            setting_id: cal.setting_id || null,
          });
        } else {
          dropped++;
        }
      } else {
        dropped++;
      }
    }

    if (dropped > 0) {
      console.error(`saveKProfiles: ${dropped} profile key(s) could not be resolved`, Array.from(selectedProfiles));
      showToast(t('inventory.kProfileSaveFailed'), 'warning');
      return false;
    }

    if (profiles.length > 0) {
      try {
        await saveApi(spoolId, profiles);
        return true;
      } catch (e) {
        console.error('Failed to save K-profiles:', e);
        showToast(t('inventory.kProfileSaveFailed'), 'warning');
        return false;
      }
    }

    return true;
  };

  // Close on Escape key
  useEffect(() => {
    if (!isOpen) return;
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  const handleSubmit = () => {
    const validation = validateForm(formData, quickAdd, spoolmanMode);
    if (!validation.isValid) {
      setErrors(validation.errors);
      if (validation.errors.slicer_filament || validation.errors.material || validation.errors.brand || validation.errors.subtype) {
        setActiveTab('filament');
      }
      return;
    }

    // Find preset name from selected option
    const presetName = selectedPresetOption?.displayName || presetInputValue || null;

    const data: Record<string, unknown> = {
      material: formData.material || null,
      subtype: formData.subtype || null,
      brand: formData.brand || null,
      color_name: formData.color_name || null,
      rgba: formData.rgba || null,
      extra_colors: formData.extra_colors || null,
      effect_type: formData.effect_type || null,
      label_weight: formData.label_weight,
      ...(spoolmanMode ? {} : { core_weight: formData.core_weight, core_weight_catalog_id: formData.core_weight_catalog_id }),
      slicer_filament: formData.slicer_filament || null,
      slicer_filament_name: presetName,
      nozzle_temp_min: null,
      nozzle_temp_max: null,
      note: formData.note || null,
      cost_per_kg: formData.cost_per_kg,
      category: formData.category.trim() || null,
      low_stock_threshold_pct: formData.low_stock_threshold_pct,
      ...(spoolmanMode ? { spoolman_filament_id: formData.spoolman_filament_id } : {}),
    };

    // Only send weight_used when creating or when explicitly changed by the user.
    // This prevents stale cached values from overwriting usage-tracker data.
    if (!isEditing || weightTouched) {
      data.weight_used = formData.weight_used;
    }

    // Only send storage_location when creating or when explicitly changed by the user.
    // This prevents the modal round-trip from overwriting the Spoolman location field
    // with a stale cached value when the user saves without touching this field.
    if (!isEditing || storageLocationTouched) {
      data.storage_location = formData.storage_location || null;
    }

    if (isEditing) {
      updateMutation.mutate(data);
    } else if (quantity > 1) {
      bulkCreateMutation.mutate({ data, qty: quantity });
    } else {
      createMutation.mutate(data);
    }
  };

  const isPending = createMutation.isPending || bulkCreateMutation.isPending || updateMutation.isPending || deleteTagMutation.isPending || unassignMutation.isPending;

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center">
      <div
        className="absolute inset-0 bg-black/60 backdrop-blur-sm"
        onClick={onClose}
      />

      <div className="relative w-full max-w-xl mx-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-2xl max-h-[90vh] flex flex-col">
        {/* Header */}
        <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary flex-shrink-0">
          <h2 className="text-lg font-semibold text-white flex items-baseline gap-2">
            {isEditing ? t('inventory.editSpool') : isCopying ? t('inventory.copySpool') : t('inventory.addSpool')}
            {isEditing && spool && (
              <span className="text-sm font-mono text-bambu-gray">#{spool.id}</span>
            )}
          </h2>
          <button
            onClick={onClose}
            className="p-1 text-bambu-gray hover:text-white rounded transition-colors"
          >
            <X className="w-5 h-5" />
          </button>
        </div>

        {/* Quick Add toggle — only in create mode (not edit, not copy).
            In copy mode the modal title is the singular "Copy Spool", so the
            quantity-driven bulkCreateMutation path would silently produce N
            copies under a misleading title — keep this toggle out of that
            mode entirely. */}
        {mode === 'create' && (
          <div className="flex items-center justify-between px-4 py-2 border-b border-bambu-dark-tertiary flex-shrink-0">
            <div className="flex items-center gap-2">
              <Zap className="w-4 h-4 text-amber-400" />
              <span className="text-sm text-white">{t('inventory.quickAdd')}</span>
            </div>
            <button
              type="button"
              onClick={() => {
                setQuickAdd(!quickAdd);
                if (!quickAdd) setActiveTab('filament');
              }}
              className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
                quickAdd ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
              }`}
            >
              <span
                className={`inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ${
                  quickAdd ? 'translate-x-4' : 'translate-x-0.5'
                }`}
              />
            </button>
          </div>
        )}

        {/* Tabs */}
        <div className="flex border-b border-bambu-dark-tertiary flex-shrink-0">
          <button
            onClick={() => setActiveTab('filament')}
            className={`flex-1 px-4 py-2.5 text-sm font-medium flex items-center justify-center gap-2 transition-colors ${
              activeTab === 'filament'
                ? 'text-bambu-green border-b-2 border-bambu-green'
                : 'text-bambu-gray hover:text-white'
            }`}
          >
            <Palette className="w-4 h-4" />
            {t('inventory.filamentInfoTab')}
          </button>
          {!quickAdd && (
            <button
              onClick={() => setActiveTab('pa-profile')}
              className={`flex-1 px-4 py-2.5 text-sm font-medium flex items-center justify-center gap-2 transition-colors ${
                activeTab === 'pa-profile'
                  ? 'text-bambu-green border-b-2 border-bambu-green'
                  : 'text-bambu-gray hover:text-white'
              }`}
            >
              <Beaker className="w-4 h-4" />
              {t('inventory.paProfileTab')}
              {selectedProfileCount > 0 && (
                <span className="text-xs px-1.5 py-0.5 rounded-full bg-bambu-green/20 text-bambu-green">
                  {selectedProfileCount}
                </span>
              )}
            </button>
          )}
        </div>

        {/* Content */}
        <div className="p-4 overflow-y-auto flex-1" style={{ scrollbarGutter: 'stable' }}>
          {activeTab === 'filament' ? (
            <div className="space-y-6">
              {/* Spoolman Filament Catalog Picker — only when creating a spool in Spoolman mode */}
              {spoolmanMode && !isEditing && (
                <div>
                  {filamentsError ? (
                    <p className="text-sm text-red-400 px-1">{t('inventory.spoolmanCatalogLoadFailed')}</p>
                  ) : (
                    <SpoolmanFilamentPicker
                      filaments={spoolmanFilaments}
                      isLoading={isLoadingFilaments}
                      selectedId={formData.spoolman_filament_id}
                      onSelect={handleFilamentSelect}
                    />
                  )}
                </div>
              )}

              {/* Filament Info Section */}
              <div>
                <h3 className="text-sm font-semibold text-bambu-gray uppercase tracking-wide mb-3">
                  {t('inventory.filamentInfo')}
                </h3>
                <FilamentSection
                  formData={formData}
                  updateField={updateField}
                  cloudAuthenticated={cloudAuthenticated}
                  loadingCloudPresets={loadingCloudPresets}
                  presetInputValue={presetInputValue}
                  setPresetInputValue={setPresetInputValue}
                  selectedPresetOption={selectedPresetOption}
                  filamentOptions={filamentOptions}
                  availableBrands={availableBrands}
                  availableMaterials={availableMaterials}
                  quickAdd={quickAdd}
                  quantity={quantity}
                  onQuantityChange={setQuantity}
                  errors={errors}
                />
              </div>

              {/* Color Section */}
              <div>
                <h3 className="text-sm font-semibold text-bambu-gray uppercase tracking-wide mb-3">
                  {t('inventory.color')}
                </h3>
                <ColorSection
                  formData={formData}
                  updateField={updateField}
                  recentColors={recentColors}
                  onColorUsed={handleColorUsed}
                  catalogColors={colorCatalog}
                />
              </div>

              {/* Additional Section */}
              <div>
                <h3 className="text-sm font-semibold text-bambu-gray uppercase tracking-wide mb-3">
                  {t('inventory.additional')}
                </h3>
                <AdditionalSection
                  formData={formData}
                  updateField={updateField}
                  spoolCatalog={spoolCatalog}
                  currencySymbol={currencySymbol}
                  availableCategories={availableCategories}
                  globalLowStockThreshold={globalLowStockThreshold}
                  spoolmanMode={spoolmanMode}
                />
              </div>

              {/* Usage History (only when editing internal inventory; Spoolman tracks its own) */}
              {isEditing && spool && !spoolmanMode && (
                <div>
                  <SpoolUsageHistory spoolId={spool.id} />
                </div>
              )}
            </div>
          ) : (
            <PAProfileSection
              formData={formData}
              updateField={updateField}
              printersWithCalibrations={resolvedCalibrations}
              selectedProfiles={selectedProfiles}
              setSelectedProfiles={setSelectedProfiles}
              expandedPrinters={expandedPrinters}
              setExpandedPrinters={setExpandedPrinters}
            />
          )}
        </div>

        {/* Footer */}
        <div className="flex gap-2 p-4 border-t border-bambu-dark-tertiary flex-shrink-0">
          {isEditing && (
            <div className="flex gap-2 mr-auto">
              <Button
                variant="secondary"
                onClick={() => deleteTagMutation.mutate()}
                disabled={isPending || !spool?.tag_uid}
              >
                <Tag className="w-4 h-4" />
                {t('inventory.clearRfid', 'Clear RFID Tag')}
              </Button>
              <Button
                variant="secondary"
                onClick={() => unassignMutation.mutate()}
                disabled={isPending || !spoolAssignment}
              >
                <Unlink className="w-4 h-4" />
                {t('inventory.unassignSpool', 'Unassign')}
              </Button>
            </div>
          )}
          <div className="flex gap-2 ml-auto">
          <Button variant="secondary" onClick={onClose}>
            {t('common.cancel')}
          </Button>
          <Button
            onClick={handleSubmit}
            disabled={isPending}
          >
            {isPending ? (
              <>
                <Loader2 className="w-4 h-4 animate-spin" />
                {t('common.saving')}
              </>
            ) : (
              <>
                <Save className="w-4 h-4" />
                {isEditing ? t('common.save') : isCopying ? t('inventory.copySpool') : t('inventory.addSpool')}
              </>
            )}
          </Button>
          </div>
        </div>
      </div>
    </div>
  );
}
