'use client';

/**
 * 수입검사 입력 모달 (패드/모바일용)
 *
 * API 기반 동적 폼:
 * - getInspectionTemplate()로 검사항목 로드
 * - 단일 측정 (measurementCount <= 1): 탭 없이 항상 표시
 * - 다중 측정 (measurementCount > 1): N1~Nn 탭으로 전환
 * - 사진 다중 첨부
 * - 자동 판정 (tolerance / standard_criteria 기반)
 * - saveInspectionData()로 저장
 */

import { useState, useEffect, useMemo, useCallback } from 'react';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  VisuallyHidden,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { FileDropzone } from '@/components/ui/file-dropzone';
import { FileList, type ExistingFile } from '@/components/ui/file-list';
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import {
  getInspectionTemplate,
  uploadInspectionFiles,
  saveInspectionData,
  type InspectionTemplateResponse,
  type DocumentResolveResponse,
} from './actions';
import { captureRenderedHtml } from '@/lib/utils/capture-rendered-html';
import { ImportInspectionDocument } from '@/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument';
import type { ImportInspectionTemplate, InspectionItemValue } from '@/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument';

// ===== Props =====
interface ImportInspectionInputModalProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  itemId?: number;
  itemName?: string;
  specification?: string;
  supplier?: string;
  inspector?: string;
  lotSize?: number;
  materialNo?: string;
  receivingId: string;
  onSave?: () => void;
}

// ===== Raw data types for auto-judgment =====
interface RawToleranceData {
  type?: string;
  value?: string | number;
  plus?: string | number;
  minus?: string | number;
  min?: string | number;
  max?: string | number;
  op?: string;
}

interface RawCriteriaData {
  min?: number | null;
  min_op?: 'gt' | 'gte' | null;
  max?: number | null;
  max_op?: 'lt' | 'lte' | null;
}

type InspectionItem = InspectionTemplateResponse['inspectionItems'][number];

// ===== standard 텍스트에서 판정 기준 파싱 =====
interface ParsedStandard {
  type: 'gte' | 'lte' | 'range' | 'percent';
  value?: number;
  min?: number;
  max?: number;
  percent?: number;
}

function parseStandardText(text: string | undefined): ParsedStandard | null {
  if (!text) return null;
  const s = text.trim();

  // "500 이상", "270 이상", "한면 17 이상", "120% 이상"
  const gteMatch = s.match(/([\d.]+)(%?)\s*이상/);
  if (gteMatch) {
    const v = parseFloat(gteMatch[1]);
    if (!isNaN(v)) return { type: 'gte', value: v };
  }

  // "200 이하"
  const lteMatch = s.match(/([\d.]+)\s*이하/);
  if (lteMatch) {
    const v = parseFloat(lteMatch[1]);
    if (!isNaN(v)) return { type: 'lte', value: v };
  }

  // "400~510"
  const rangeMatch = s.match(/([\d.]+)\s*[~～]\s*([\d.]+)/);
  if (rangeMatch) {
    const min = parseFloat(rangeMatch[1]);
    const max = parseFloat(rangeMatch[2]);
    if (!isNaN(min) && !isNaN(max)) return { type: 'range', min, max };
  }

  // "규격 ±10%", "규격 ±5%", "규격 ±2%"
  const percentMatch = s.match(/규격\s*[±]\s*([\d.]+)%/);
  if (percentMatch) {
    const pct = parseFloat(percentMatch[1]);
    if (!isNaN(pct)) return { type: 'percent', percent: pct };
  }

  return null;
}

// ===== Auto-judgment logic =====
function calculateAutoResult(
  value: string,
  rawTolerance: RawToleranceData | null | undefined,
  rawCriteria: RawCriteriaData | null | undefined,
  baseValue: number | null | undefined,
  standardText?: string,
): 'ok' | 'ng' | null {
  const num = parseFloat(value);
  if (isNaN(num)) return null;

  // 1. Tolerance-based (needs base value)
  if (rawTolerance && baseValue != null) {
    switch (rawTolerance.type) {
      case 'symmetric': {
        const tol = parseFloat(String(rawTolerance.value || 0));
        return Math.abs(num - baseValue) <= tol ? 'ok' : 'ng';
      }
      case 'asymmetric': {
        const plus = parseFloat(String(rawTolerance.plus || 0));
        const minus = parseFloat(String(rawTolerance.minus || 0));
        return (num >= baseValue - minus && num <= baseValue + plus) ? 'ok' : 'ng';
      }
      case 'range': {
        const min = rawTolerance.min != null ? parseFloat(String(rawTolerance.min)) : -Infinity;
        const max = rawTolerance.max != null ? parseFloat(String(rawTolerance.max)) : Infinity;
        return (num >= min && num <= max) ? 'ok' : 'ng';
      }
      case 'limit': {
        const lv = parseFloat(String(rawTolerance.value || 0));
        switch (rawTolerance.op) {
          case 'gte': return num >= lv ? 'ok' : 'ng';
          case 'gt': return num > lv ? 'ok' : 'ng';
          case 'lte': return num <= lv ? 'ok' : 'ng';
          case 'lt': return num < lv ? 'ok' : 'ng';
          default: return null;
        }
      }
    }
  }

  // 2. Criteria-based (min/max range)
  if (rawCriteria && (rawCriteria.min != null || rawCriteria.max != null)) {
    let pass = true;
    if (rawCriteria.min != null) {
      pass = pass && (rawCriteria.min_op === 'gt' ? num > rawCriteria.min : num >= rawCriteria.min);
    }
    if (rawCriteria.max != null) {
      pass = pass && (rawCriteria.max_op === 'lt' ? num < rawCriteria.max : num <= rawCriteria.max);
    }
    return pass ? 'ok' : 'ng';
  }

  // 3. standard 텍스트 파싱 기반 판정 (tolerance/criteria 미설정 시 fallback)
  const parsed = parseStandardText(standardText);
  if (parsed) {
    switch (parsed.type) {
      case 'gte': return num >= parsed.value! ? 'ok' : 'ng';
      case 'lte': return num <= parsed.value! ? 'ok' : 'ng';
      case 'range': return (num >= parsed.min! && num <= parsed.max!) ? 'ok' : 'ng';
      case 'percent':
        if (baseValue != null) {
          const tolerance = baseValue * parsed.percent! / 100;
          return (num >= baseValue - tolerance && num <= baseValue + tolerance) ? 'ok' : 'ng';
        }
        return null;
    }
  }

  return null;
}

// ===== OK/NG 토글 =====
function OkNgToggle({
  value,
  onChange,
}: {
  value: 'ok' | 'ng' | null;
  onChange: (v: 'ok' | 'ng') => void;
}) {
  return (
    <div className="flex gap-2 flex-1">
      <button
        type="button"
        onClick={() => onChange('ok')}
        className={cn(
          'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
          value === 'ok'
            ? 'bg-black text-white'
            : 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
        )}
      >
        OK
      </button>
      <button
        type="button"
        onClick={() => onChange('ng')}
        className={cn(
          'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
          value === 'ng'
            ? 'bg-black text-white'
            : 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
        )}
      >
        NG
      </button>
    </div>
  );
}

// ===== 적합/부적합 토글 =====
function JudgmentToggle({
  value,
  onChange,
}: {
  value: 'pass' | 'fail' | null;
  onChange: (v: 'pass' | 'fail') => void;
}) {
  return (
    <div className="flex gap-2 flex-1">
      <button
        type="button"
        onClick={() => onChange('pass')}
        className={cn(
          'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
          value === 'pass'
            ? 'bg-orange-600 text-white'
            : 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
        )}
      >
        적합
      </button>
      <button
        type="button"
        onClick={() => onChange('fail')}
        className={cn(
          'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
          value === 'fail'
            ? 'bg-black text-white'
            : 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
        )}
      >
        부적합
      </button>
    </div>
  );
}

// ===== Main Component =====
export function ImportInspectionInputModal({
  open,
  onOpenChange,
  itemId,
  itemName,
  specification,
  supplier,
  inspector,
  lotSize,
  materialNo,
  receivingId,
  onSave,
}: ImportInspectionInputModalProps) {
  // Template
  const [template, setTemplate] = useState<InspectionTemplateResponse | null>(null);
  const [resolveData, setResolveData] = useState<DocumentResolveResponse | null>(null);
  const [isLoadingTemplate, setIsLoadingTemplate] = useState(false);
  const [isSaving, setIsSaving] = useState(false);

  // Form
  const [currentTab, setCurrentTab] = useState(0);
  const [measurements, setMeasurements] = useState<Record<string, Record<number, string>>>({});
  const [okngValues, setOkngValues] = useState<Record<string, Record<number, 'ok' | 'ng' | null>>>({});
  const [overallResult, setOverallResult] = useState<'pass' | 'fail' | null>(null);
  const [remark, setRemark] = useState('');

  // Test fill toggle
  const [isTestFilled, setIsTestFilled] = useState(false);

  // Photos
  const [newPhotos, setNewPhotos] = useState<File[]>([]);
  const [existingPhotos, setExistingPhotos] = useState<ExistingFile[]>([]);

  // ===== 단일 / 다중 항목 분리 =====
  // isFirstInItem === false인 항목은 같은 category+item의 옵션 행(중복)이므로 제외
  const { singleItems, multiItems, maxN } = useMemo(() => {
    if (!template) return { singleItems: [] as InspectionItem[], multiItems: [] as InspectionItem[], maxN: 0 };
    const unique = template.inspectionItems.filter(i => i.isFirstInItem !== false);
    const single = unique.filter(i => i.measurementCount <= 1);
    const multi = unique.filter(i => i.measurementCount > 1);
    const n = multi.length > 0 ? Math.max(...multi.map(i => i.measurementCount)) : 0;
    return { singleItems: single, multiItems: multi, maxN: n };
  }, [template]);

  // ===== Raw data map (tolerance / criteria) for auto-judgment =====
  const rawDataMap = useMemo(() => {
    const map = new Map<string, { tolerance: RawToleranceData | null; criteria: RawCriteriaData | null; standard: string | null }>();
    if (!resolveData) return map;
    for (const section of resolveData.template.sections) {
      for (const item of section.items) {
        map.set(String(item.id), {
          tolerance: (item.tolerance && typeof item.tolerance === 'object') ? item.tolerance as RawToleranceData : null,
          criteria: item.standard_criteria || null,
          standard: (item as Record<string, unknown>).standard as string | null,
        });
      }
    }
    return map;
  }, [resolveData]);

  // ===== Base values from item attributes (두께, 너비, 길이) =====
  const baseValueMap = useMemo(() => {
    const map = new Map<string, number>();
    if (!resolveData?.item?.attributes || !template) return map;
    const attrs = resolveData.item.attributes as { thickness?: number; width?: number; length?: number };
    const dimensionNames: Record<string, number | undefined> = {
      '두께': attrs.thickness,
      '너비': attrs.width,
      '길이': attrs.length,
    };
    for (const item of template.inspectionItems) {
      const key = item.subName || item.name;
      if (dimensionNames[key] != null) {
        map.set(item.id, dimensionNames[key]!);
      }
    }
    return map;
  }, [resolveData, template]);

  // ===== Item → section/row mapping for normalized records =====
  const itemSectionMap = useMemo(() => {
    if (!resolveData) return new Map<string, { sectionId: number; rowIndex: number }>();
    const map = new Map<string, { sectionId: number; rowIndex: number }>();
    for (const section of resolveData.template.sections) {
      section.items.forEach((item, idx) => {
        map.set(String(item.id), { sectionId: section.id, rowIndex: idx });
      });
    }
    return map;
  }, [resolveData]);

  // ===== Complex column ID (measurements) and result column ID (judgment) =====
  const { complexColumnId, resultColumnId } = useMemo(() => {
    if (!resolveData) return { complexColumnId: null as number | null, resultColumnId: null as number | null };
    const cols = resolveData.template.columns;
    const complex = cols.find(c => c.input_type === 'complex') ?? null;
    const result = cols.find(c => c.input_type === 'select' || c.input_type === 'check') ??
                   cols.find(c => c.label.includes('판정')) ?? null;
    return { complexColumnId: complex?.id ?? null, resultColumnId: result?.id ?? null };
  }, [resolveData]);

  // ===== Load template on modal open =====
  useEffect(() => {
    if (!open || !itemId) return;

    setIsLoadingTemplate(true);
    getInspectionTemplate({
      itemId,
      itemName,
      specification,
      supplier,
      inspector,
      lotSize,
      materialNo,
    })
      .then((result) => {
        if (result.success && result.data) {
          setTemplate(result.data);
          if (result.resolveData) {
            setResolveData(result.resolveData);
            loadExistingData(result.data, result.resolveData);
          } else {
            resetForm();
          }
        } else {
          toast.error(result.error || '검사 템플릿 로드 실패');
        }
      })
      .finally(() => setIsLoadingTemplate(false));
  }, [open, itemId]);  

  // ===== Load existing saved data (재검사) =====
  const loadExistingData = useCallback(
    (tmpl: InspectionTemplateResponse, resolve: DocumentResolveResponse) => {
      const doc = resolve.document;
      if (!doc?.data?.length) {
        resetForm();
        return;
      }

      // Reverse map: (sectionId, rowIndex) → inspectionItem.id
      const reverseMap = new Map<string, string>();
      for (const section of resolve.template.sections) {
        section.items.forEach((item, idx) => {
          reverseMap.set(`${section.id}_${idx}`, String(item.id));
        });
      }

      const meas: Record<string, Record<number, string>> = {};
      const okng: Record<string, Record<number, 'ok' | 'ng' | null>> = {};
      let savedRemark = '';
      let savedOverall: 'pass' | 'fail' | null = null;

      for (const d of doc.data) {
        const key = d.field_key;
        const val = d.field_value || '';

        // Footer fields
        if (key === 'remark' || key === 'footer_remark') { savedRemark = val; continue; }
        if (key === 'overall_result' || key === 'footer_judgement') {
          savedOverall = (val === 'pass' || val === '적합') ? 'pass'
            : (val === 'fail' || val === '부적합') ? 'fail' : null;
          continue;
        }

        // 정규화 형식: section_id가 있으면 reverse map으로 item 찾기
        if (d.section_id != null) {
          const itemId = reverseMap.get(`${d.section_id}_${d.row_index}`);
          if (!itemId) continue;

          const nMatch = key.match(/^n(\d+)$/);
          if (nMatch) {
            const idx = parseInt(nMatch[1]) - 1;
            if (!meas[itemId]) meas[itemId] = {};
            meas[itemId][idx] = val;
            continue;
          }
          const okMatch = key.match(/^n(\d+)_ok$/);
          if (okMatch && val === 'OK') {
            const idx = parseInt(okMatch[1]) - 1;
            if (!okng[itemId]) okng[itemId] = {};
            okng[itemId][idx] = 'ok';
            continue;
          }
          const ngMatch = key.match(/^n(\d+)_ng$/);
          if (ngMatch && val === 'NG') {
            const idx = parseInt(ngMatch[1]) - 1;
            if (!okng[itemId]) okng[itemId] = {};
            okng[itemId][idx] = 'ng';
            continue;
          }
          continue;
        }

        // 레거시 형식 fallback: {itemId}_n{N}
        const nLegacy = key.match(/^(\d+)_n(\d+)$/);
        if (nLegacy) {
          const id = nLegacy[1];
          const idx = parseInt(nLegacy[2]) - 1;
          if (!meas[id]) meas[id] = {};
          meas[id][idx] = val;
          continue;
        }
        // 레거시: {itemId}_okng_n{N}
        const okngLegacy = key.match(/^(\d+)_okng_n(\d+)$/);
        if (okngLegacy) {
          const id = okngLegacy[1];
          const idx = parseInt(okngLegacy[2]) - 1;
          if (!okng[id]) okng[id] = {};
          okng[id][idx] = val === 'ok' ? 'ok' : val === 'ng' ? 'ng' : null;
          continue;
        }
      }

      setMeasurements(meas);
      setOkngValues(okng);
      setRemark(savedRemark);
      setOverallResult(savedOverall);
      setCurrentTab(0);
      setNewPhotos([]);

      // 기존 첨부 사진 로드
      if (doc.attachments?.length) {
        setExistingPhotos(
          doc.attachments
            .filter((a) => a.attachment_type === 'image' && a.file)
            .map((a) => ({
              id: a.file_id,
              name: a.file!.display_name || a.file!.original_name || `file-${a.file_id}`,
              size: a.file!.file_size,
              url: `/api/proxy/files/${a.file_id}/download`,
            }))
        );
      } else {
        setExistingPhotos([]);
      }
    },
    []
  );

  const resetForm = useCallback(() => {
    setMeasurements({});
    setOkngValues({});
    setOverallResult(null);
    setRemark('');
    setNewPhotos([]);
    setExistingPhotos([]);
    setCurrentTab(0);
  }, []);

  // ===== 테스트 데이터 채우기 (전체 합격값) =====
  const generatePassingValue = useCallback(
    (item: InspectionItem): string => {
      const raw = rawDataMap.get(item.id);
      const base = baseValueMap.get(item.id);

      // 1. 기준값이 있으면 그대로 사용 (공차 범위 중앙)
      if (base != null) return String(base);

      // 2. Tolerance 기반
      if (raw?.tolerance) {
        switch (raw.tolerance.type) {
          case 'range': {
            const min = parseFloat(String(raw.tolerance.min || 0));
            const max = parseFloat(String(raw.tolerance.max || 0));
            return String(Math.round(((min + max) / 2) * 100) / 100);
          }
          case 'limit': {
            const lv = parseFloat(String(raw.tolerance.value || 0));
            if (raw.tolerance.op === 'gte') return String(lv);
            if (raw.tolerance.op === 'gt') return String(Math.round((lv + 1) * 100) / 100);
            if (raw.tolerance.op === 'lte') return String(lv);
            if (raw.tolerance.op === 'lt') return String(Math.round((lv - 1) * 100) / 100);
            break;
          }
        }
      }

      // 3. Criteria 기반
      if (raw?.criteria) {
        const { min, max } = raw.criteria;
        if (min != null && max != null) return String(Math.round(((min + max) / 2) * 100) / 100);
        if (min != null) return String(min);
        if (max != null) return String(max);
      }

      // 4. standard 텍스트 파싱 기반 (tolerance/criteria 미설정 시 fallback)
      const stdText = raw?.standard || item.standard?.description;
      const parsed = parseStandardText(stdText);
      if (parsed) {
        switch (parsed.type) {
          case 'gte': return String(parsed.value!);
          case 'lte': return String(parsed.value!);
          case 'range': return String(Math.round(((parsed.min! + parsed.max!) / 2) * 100) / 100);
          case 'percent':
            if (base != null) {
              return String(base);
            }
            break;
        }
      }

      return '100';
    },
    [rawDataMap, baseValueMap]
  );

  const fillTestData = useCallback(() => {
    if (!template) return;
    const targetItems = template.inspectionItems.filter(i => i.isFirstInItem !== false);
    const newMeas: Record<string, Record<number, string>> = {};
    const newOkng: Record<string, Record<number, 'ok' | 'ng' | null>> = {};

    for (const item of targetItems) {
      for (let n = 0; n < item.measurementCount; n++) {
        if (item.measurementType === 'okng') {
          if (!newOkng[item.id]) newOkng[item.id] = {};
          newOkng[item.id][n] = 'ok';
        } else if (item.measurementType === 'substitute') {
          if (!newMeas[item.id]) newMeas[item.id] = {};
          newMeas[item.id][n] = '정상';
        } else {
          if (!newMeas[item.id]) newMeas[item.id] = {};
          newMeas[item.id][n] = generatePassingValue(item);
        }
      }
    }

    setMeasurements(newMeas);
    setOkngValues(newOkng);
    setIsTestFilled(true);
  }, [template, generatePassingValue]);

  const clearTestData = useCallback(() => {
    resetForm();
    setIsTestFilled(false);
  }, [resetForm]);

  // ===== 단일 측정값의 자동 판정 =====
  const getMeasurementResult = useCallback(
    (itemId: string, nIndex: number): 'ok' | 'ng' | null => {
      const v = measurements[itemId]?.[nIndex];
      if (!v) return null;
      const raw = rawDataMap.get(itemId);
      const base = baseValueMap.get(itemId);
      // template inspectionItems에서 standard.description 추출
      const tmplItem = template?.inspectionItems.find(i => i.id === itemId);
      const stdText = raw?.standard || tmplItem?.standard?.description;
      return calculateAutoResult(v, raw?.tolerance, raw?.criteria, base, stdText);
    },
    [measurements, rawDataMap, baseValueMap, template]
  );

  // ===== 항목별 종합 판정 (c=0: 모든 N 측정값이 입력+통과해야 OK) =====
  const getItemResult = useCallback(
    (item: InspectionItem): 'ok' | 'ng' | null => {
      const isOkng = item.measurementType === 'okng';
      let allFilled = true;
      let hasNg = false;

      for (let n = 0; n < item.measurementCount; n++) {
        if (isOkng) {
          const v = okngValues[item.id]?.[n];
          if (!v) {
            allFilled = false;
          } else if (v === 'ng') {
            hasNg = true;
          }
        } else {
          const v = measurements[item.id]?.[n];
          if (!v) {
            allFilled = false;
          } else {
            const raw = rawDataMap.get(item.id);
            const base = baseValueMap.get(item.id);
            const stdText = raw?.standard || item.standard?.description;
            const r = calculateAutoResult(v, raw?.tolerance, raw?.criteria, base, stdText);
            if (r === 'ng') hasNg = true;
          }
        }
      }

      // NG가 하나라도 있으면 즉시 부적합
      if (hasNg) return 'ng';
      // 모든 N이 입력 완료되어야 OK
      if (allFilled) return 'ok';
      // 미완료
      return null;
    },
    [measurements, okngValues, rawDataMap, baseValueMap]
  );

  // ===== 종합 판정 자동 계산 (모든 항목 완료+OK여야 적합) =====
  useEffect(() => {
    if (!template) return;

    // 실제 렌더링 대상 항목만 (중복 옵션 행 제외)
    const targetItems = template.inspectionItems.filter(i => i.isFirstInItem !== false);

    let hasNg = false;
    let allComplete = true;

    for (const item of targetItems) {
      const r = getItemResult(item);
      if (r === 'ng') {
        hasNg = true;
      } else if (r === null) {
        allComplete = false;
      }
    }

    // NG가 있으면 즉시 부적합
    if (hasNg) {
      setOverallResult('fail');
    } else if (allComplete) {
      // 모든 항목이 완료되고 전부 OK → 적합
      setOverallResult('pass');
    } else {
      // 미완료 상태에서는 판정 초기화
      setOverallResult(null);
    }
  }, [template, getItemResult]);

  // ===== Handlers =====
  const handleMeasurementChange = useCallback(
    (id: string, nIndex: number, value: string) => {
      setMeasurements((prev) => ({
        ...prev,
        [id]: { ...prev[id], [nIndex]: value },
      }));
    },
    []
  );

  const handleOkngChange = useCallback(
    (id: string, nIndex: number, value: 'ok' | 'ng') => {
      setOkngValues((prev) => ({
        ...prev,
        [id]: { ...prev[id], [nIndex]: value },
      }));
    },
    []
  );

  // ===== 저장 =====
  const handleSave = async () => {
    if (!template || !resolveData || !itemId) return;

    setIsSaving(true);
    try {
      // 1. 사진 업로드
      let uploadedFileIds: number[] = [];
      if (newPhotos.length > 0) {
        const uploadResult = await uploadInspectionFiles(newPhotos);
        if (!uploadResult.success) {
          toast.error(uploadResult.error || '사진 업로드 실패');
          return;
        }
        uploadedFileIds = (uploadResult.data || []).map((f) => f.id);
      }

      // 2. 정규화 형식 데이터 배열 생성
      const data: Array<{
        section_id: number | null;
        column_id: number | null;
        row_index: number;
        field_key: string;
        field_value: string | null;
      }> = [];

      for (const item of template.inspectionItems) {
        const mapping = itemSectionMap.get(item.id);
        const sectionId = mapping?.sectionId ?? null;
        const rowIndex = mapping?.rowIndex ?? 0;
        const isOkng = item.measurementType === 'okng';

        for (let n = 0; n < item.measurementCount; n++) {
          if (isOkng) {
            const val = okngValues[item.id]?.[n];
            data.push({
              section_id: sectionId, column_id: complexColumnId, row_index: rowIndex,
              field_key: `n${n + 1}_ok`, field_value: val === 'ok' ? 'OK' : '',
            });
            data.push({
              section_id: sectionId, column_id: complexColumnId, row_index: rowIndex,
              field_key: `n${n + 1}_ng`, field_value: val === 'ng' ? 'NG' : '',
            });
          } else {
            data.push({
              section_id: sectionId, column_id: complexColumnId, row_index: rowIndex,
              field_key: `n${n + 1}`, field_value: measurements[item.id]?.[n] || null,
            });
          }
        }

        // 항목별 판정 (판정 컬럼이 있을 때만)
        if (resultColumnId) {
          const itemResult = getItemResult(item);
          data.push({
            section_id: sectionId, column_id: resultColumnId, row_index: rowIndex,
            field_key: 'value',
            field_value: itemResult === 'ok' ? '적합' : itemResult === 'ng' ? '부적합' : null,
          });
        }
      }

      // 종합판정 + 비고
      data.push({
        section_id: null, column_id: null, row_index: 0,
        field_key: 'overall_result',
        field_value: overallResult === 'pass' ? '적합' : overallResult === 'fail' ? '부적합' : null,
      });
      data.push({
        section_id: null, column_id: null, row_index: 0,
        field_key: 'remark', field_value: remark || null,
      });

      // 3. 첨부파일 (기존 + 신규)
      const attachments = [
        ...existingPhotos.map((p) => ({
          file_id: Number(p.id),
          attachment_type: 'image',
        })),
        ...uploadedFileIds.map((id) => ({
          file_id: id,
          attachment_type: 'image',
        })),
      ];

      // 4. 성적서 문서를 오프스크린 렌더링하여 HTML 스냅샷 캡처 (MNG 출력용)
      let renderedHtml: string | undefined;
      try {
        // 현재 입력값을 ImportInspectionDocument의 initialValues 형식으로 변환
        const docValues: InspectionItemValue[] = template.inspectionItems
          .filter(i => i.isFirstInItem !== false)
          .map(item => ({
            itemId: item.id,
            measurements: Array.from({ length: item.measurementCount }, (_, n) => {
              if (item.measurementType === 'okng') {
                const v = okngValues[item.id]?.[n];
                return v === 'ok' ? ('OK' as const) : v === 'ng' ? ('NG' as const) : null;
              }
              const v = measurements[item.id]?.[n];
              return v ? Number(v) : null;
            }),
            result: getItemResult(item) === 'ok' ? ('OK' as const) : getItemResult(item) === 'ng' ? ('NG' as const) : null,
          }));
        // 성적서 문서 컴포넌트를 오프스크린에서 렌더링
        renderedHtml = captureRenderedHtml(
          <ImportInspectionDocument
            template={template as unknown as ImportInspectionTemplate}
            initialValues={docValues}
            readOnly
          />
        );
      } catch {
        // 캡처 실패 시 무시 — rendered_html 없이 저장 진행
      }

      // 5. 저장 API 호출 (rendered_html 500KB 초과 시 제외 — 413 에러 방지)
      const MAX_HTML_SIZE = 500 * 1024;
      const safeHtml = renderedHtml && renderedHtml.length <= MAX_HTML_SIZE
        ? renderedHtml : undefined;

      const result = await saveInspectionData({
        templateId: parseInt(template.templateId),
        itemId,
        title: `수입검사 - ${template.headerInfo.productName}`,
        data,
        attachments,
        receivingId,
        inspectionResult: overallResult,
        rendered_html: safeHtml,
      });

      if (result.success) {
        toast.success('수입검사가 저장되었습니다.');
        onSave?.();
        onOpenChange(false);
      } else {
        toast.error(result.error || '저장에 실패했습니다.');
      }
    } finally {
      setIsSaving(false);
    }
  };

  // ===== 항목 라벨 텍스트 (기준값 포함) =====
  const getItemLabelText = (item: InspectionItem) => {
    const name = item.subName || item.name;
    const base = baseValueMap.get(item.id);
    const desc = item.standard.description;

    if (base != null) {
      return `${name} (${base})`;
    }
    if (desc && desc !== '-' && desc !== name) {
      return `${name} (${desc})`;
    }
    return name;
  };

  // ===== 단일 항목 렌더링 (라벨 위, 입력 아래) =====
  const renderItemInput = (item: InspectionItem, nIndex: number) => {
    const isOkng = item.measurementType === 'okng';
    const labelText = getItemLabelText(item);

    if (isOkng) {
      return (
        <div key={`${item.id}-${nIndex}`} className="col-span-2 space-y-2">
          <span className="text-sm font-bold">{labelText}</span>
          <OkNgToggle
            value={okngValues[item.id]?.[nIndex] || null}
            onChange={(v) => handleOkngChange(item.id, nIndex, v)}
          />
        </div>
      );
    }

    // numeric / single_value / substitute / both → 라벨 위 + 입력 아래
    const result = getMeasurementResult(item.id, nIndex);
    const isSubstitute = item.measurementType === 'substitute';

    return (
      <div key={`${item.id}-${nIndex}`} className="space-y-1.5">
        <span className="text-sm font-bold">{labelText}</span>
        <div className="relative">
          <Input
            type={isSubstitute ? 'text' : 'number'}
            inputMode={isSubstitute ? 'text' : 'decimal'}
            step="any"
            value={measurements[item.id]?.[nIndex] ?? ''}
            onChange={(e) => handleMeasurementChange(item.id, nIndex, e.target.value)}
            className={cn(
              'h-11 w-full rounded-lg border-gray-300',
              result === 'ok' && 'border-green-500 bg-green-50',
              result === 'ng' && 'border-red-500 bg-red-50'
            )}
          />
        </div>
      </div>
    );
  };

  // ===== Render =====
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="w-[95vw] max-w-[500px] sm:max-w-[500px] max-h-[90vh] flex flex-col p-0">
        <DialogHeader className="px-6 pt-6 pb-0 shrink-0">
          <div className="flex items-center gap-2">
            <DialogTitle className="text-lg font-bold">수입검사</DialogTitle>
            {process.env.NODE_ENV === 'development' && template && (
              <button
                type="button"
                onClick={isTestFilled ? clearTestData : fillTestData}
                className={cn(
                  'px-3 py-1 text-xs font-bold rounded-full transition-colors',
                  isTestFilled
                    ? 'bg-red-500 hover:bg-red-600 text-white'
                    : 'bg-amber-500 hover:bg-amber-600 text-white'
                )}
              >
                {isTestFilled ? '초기화' : '테스트입력'}
              </button>
            )}
          </div>
          <VisuallyHidden><DialogDescription>수입검사 항목 입력</DialogDescription></VisuallyHidden>
        </DialogHeader>

        {isLoadingTemplate ? (
          <div className="flex items-center justify-center py-12 px-6">
            <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
            <span className="ml-2 text-sm text-muted-foreground">검사항목 로딩 중...</span>
          </div>
        ) : !template ? (
          <div className="text-center py-12 text-muted-foreground text-sm px-6">
            검사 템플릿을 불러올 수 없습니다.
          </div>
        ) : (
          <div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-5">
            {/* 기본 정보 (읽기전용 인풋 스타일) */}
            <div className="space-y-3">
              <div className="space-y-1.5">
                <span className="text-sm font-bold">제품명</span>
                <Input
                  value={template.headerInfo.productName}
                  readOnly
                  className="h-11 bg-gray-100 border-gray-300 rounded-lg"
                />
              </div>
              <div className="space-y-1.5">
                <span className="text-sm font-bold">규격</span>
                <Input
                  value={template.headerInfo.specification}
                  readOnly
                  className="h-11 bg-gray-100 border-gray-300 rounded-lg"
                />
              </div>
            </div>

            {/* N탭 측정 항목 (measurementCount > 1) — 그룹 카드 */}
            {multiItems.length > 0 && maxN > 0 && (
              <div className="border border-gray-300 rounded-xl overflow-hidden">
                {/* N 탭 (밑줄 스타일) */}
                <div className="flex border-b border-gray-300 bg-gray-50">
                  {Array.from({ length: maxN }, (_, i) => (
                    <button
                      key={i}
                      type="button"
                      onClick={() => setCurrentTab(i)}
                      className={cn(
                        'flex-1 py-2.5 text-sm font-medium transition-colors relative',
                        currentTab === i
                          ? 'text-black'
                          : 'text-gray-400 hover:text-gray-600'
                      )}
                    >
                      N{i + 1}
                      {currentTab === i && (
                        <span className="absolute bottom-0 left-0 right-0 h-[2px] bg-black" />
                      )}
                    </button>
                  ))}
                </div>

                {/* 현재 탭 항목 (2단 그리드) */}
                <div className="grid grid-cols-2 gap-3 p-4">
                  {multiItems.map((item) => {
                    if (currentTab >= item.measurementCount) return null;
                    return renderItemInput(item, currentTab);
                  })}
                </div>
              </div>
            )}

            {/* 단일 측정 항목 (measurementCount <= 1, 2단 그리드) */}
            {singleItems.length > 0 && (
              <div className="grid grid-cols-2 gap-3">
                {singleItems.map((item) => renderItemInput(item, 0))}
              </div>
            )}

            {/* 사진 첨부 */}
            <div className="space-y-2">
              <FileDropzone
                accept=".jpg,.jpeg,.png,.gif,.webp,.heic,.pdf"
                multiple
                maxSize={10}
                compact
                onFilesSelect={(files) => setNewPhotos((prev) => [...prev, ...files])}
                title="클릭하거나 파일을 드래그하세요"
                description="이미지/PDF 파일 (최대 10MB)"
              />
              {(newPhotos.length > 0 || existingPhotos.length > 0) && (
                <FileList
                  files={newPhotos.map((f) => ({ file: f }))}
                  existingFiles={existingPhotos}
                  onRemove={(index) =>
                    setNewPhotos((prev) => prev.filter((_, i) => i !== index))
                  }
                  onRemoveExisting={(id) =>
                    setExistingPhotos((prev) => prev.filter((p) => p.id !== id))
                  }
                  compact
                />
              )}
            </div>

            {/* 내용 (비고) */}
            <div className="space-y-1.5">
              <span className="text-sm font-bold">내용</span>
              <Textarea
                value={remark}
                onChange={(e) => setRemark(e.target.value)}
                className="min-h-[80px] rounded-lg border-gray-300"
              />
            </div>
          </div>
        )}

        {/* 하단 고정: 판정 + 버튼 */}
        <div className="shrink-0 border-t bg-white px-5 pb-5 pt-3 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
          {template && (
            <div className="space-y-2 mb-4">
              <span className="text-sm font-bold">판정</span>
              <JudgmentToggle
                value={overallResult}
                onChange={(v) => setOverallResult(v)}
              />
            </div>
          )}
          <div className="flex gap-3">
            <Button
              variant="outline"
              onClick={() => onOpenChange(false)}
              className="flex-1 h-11 rounded-lg border-gray-300 font-bold"
              disabled={isSaving}
            >
              취소
            </Button>
            <Button
              onClick={handleSave}
              className="flex-1 h-11 rounded-lg bg-black hover:bg-gray-800 text-white font-bold"
              disabled={isSaving || !template}
            >
              {isSaving ? (
                <>
                  <Loader2 className="w-4 h-4 mr-2 animate-spin" />
                  저장 중...
                </>
              ) : (
                '검사 완료'
              )}
            </Button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  );
}
