/**
 * BOM 이력 및 버전 관리 서비스
 * 행(Row) 기반 버전 관리: bom_detail.version_id로 버전별 데이터 분리
 */

import { query, queryOne, transaction } from "../database/db";
import { logger } from "../utils/logger";

function safeTableName(name: string, fallback: string): string {
  if (!name || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) return fallback;
  return name;
}

// ─── 이력 (History) ─────────────────────────────

export async function getBomHistory(bomId: string, companyCode: string, tableName?: string) {
  const table = safeTableName(tableName || "", "bom_history");
  const sql = companyCode === "*"
    ? `SELECT * FROM ${table} WHERE bom_id = $1 ORDER BY changed_date DESC`
    : `SELECT * FROM ${table} WHERE bom_id = $1 AND company_code = $2 ORDER BY changed_date DESC`;
  const params = companyCode === "*" ? [bomId] : [bomId, companyCode];
  return query(sql, params);
}

export async function addBomHistory(
  bomId: string,
  companyCode: string,
  data: {
    revision?: string;
    version?: string;
    change_type: string;
    change_description?: string;
    changed_by?: string;
  },
  tableName?: string,
) {
  const table = safeTableName(tableName || "", "bom_history");
  const sql = `
    INSERT INTO ${table} (bom_id, revision, version, change_type, change_description, changed_by, company_code)
    VALUES ($1, $2, $3, $4, $5, $6, $7)
    RETURNING *
  `;
  return queryOne(sql, [
    bomId,
    data.revision || null,
    data.version || null,
    data.change_type,
    data.change_description || null,
    data.changed_by || null,
    companyCode,
  ]);
}

// ─── 버전 (Version) ─────────────────────────────

// ─── BOM 헤더 조회 (entity join 포함) ─────────────────────────────

export async function getBomHeader(bomId: string, tableName?: string) {
  const table = safeTableName(tableName || "", "bom");
  const sql = `
    SELECT b.*,
           i.item_name, i.item_number, i.division as item_type,
           COALESCE(b.unit, i.unit) as unit,
           i.unit as item_unit,
           i.division, i.size, i.material
    FROM ${table} b
    LEFT JOIN item_info i ON b.item_id = i.id
    WHERE b.id = $1
    LIMIT 1
  `;
  return queryOne<Record<string, any>>(sql, [bomId]);
}

export async function getBomVersions(bomId: string, companyCode: string, tableName?: string) {
  const table = safeTableName(tableName || "", "bom_version");
  const dTable = "bom_detail";

  // 버전 목록 + 각 버전별 디테일 건수 + 현재 활성 버전 ID
  const sql = companyCode === "*"
    ? `SELECT v.*, (SELECT COUNT(*) FROM ${dTable} d WHERE d.version_id = v.id) as detail_count
       FROM ${table} v WHERE v.bom_id = $1 ORDER BY v.created_date DESC`
    : `SELECT v.*, (SELECT COUNT(*) FROM ${dTable} d WHERE d.version_id = v.id) as detail_count
       FROM ${table} v WHERE v.bom_id = $1 AND v.company_code = $2 ORDER BY v.created_date DESC`;
  const params = companyCode === "*" ? [bomId] : [bomId, companyCode];
  const versions = await query(sql, params);

  // bom.current_version_id도 함께 반환
  const bomRow = await queryOne<{ current_version_id: string }>(
    `SELECT current_version_id FROM bom WHERE id = $1`, [bomId],
  );

  return {
    versions,
    currentVersionId: bomRow?.current_version_id || null,
  };
}

/**
 * 새 버전 생성: 현재 활성 버전의 bom_detail 행을 복사하여 새 version_id로 INSERT
 */
export async function createBomVersion(
  bomId: string, companyCode: string, createdBy: string,
  versionTableName?: string, detailTableName?: string,
  inputVersionName?: string,
) {
  const vTable = safeTableName(versionTableName || "", "bom_version");
  const dTable = safeTableName(detailTableName || "", "bom_detail");

  return transaction(async (client) => {
    const bomRow = await client.query(`SELECT * FROM bom WHERE id = $1`, [bomId]);
    if (bomRow.rows.length === 0) throw new Error("BOM을 찾을 수 없습니다");
    const bomData = bomRow.rows[0];

    // 버전명: 사용자 입력 > 순번 자동 생성
    let versionName = inputVersionName?.trim();
    if (!versionName) {
      const countResult = await client.query(
        `SELECT COUNT(*)::int as cnt FROM ${vTable} WHERE bom_id = $1`,
        [bomId],
      );
      versionName = `${(countResult.rows[0].cnt || 0) + 1}.0`;
    }

    // 중복 체크
    const dupCheck = await client.query(
      `SELECT id FROM ${vTable} WHERE bom_id = $1 AND version_name = $2`,
      [bomId, versionName],
    );
    if (dupCheck.rows.length > 0) {
      throw new Error(`이미 존재하는 버전명입니다: ${versionName}`);
    }

    // 새 버전 레코드 생성 (snapshot_data 없이)
    const insertSql = `
      INSERT INTO ${vTable} (bom_id, version_name, revision, status, created_by, company_code)
      VALUES ($1, $2, $3, 'developing', $4, $5)
      RETURNING *
    `;
    const newVersion = await client.query(insertSql, [
      bomId,
      versionName,
      bomData.revision ? parseInt(bomData.revision, 10) || 0 : 0,
      createdBy,
      companyCode,
    ]);
    const newVersionId = newVersion.rows[0].id;

    // 현재 활성 버전의 bom_detail 행을 복사
    const sourceVersionId = bomData.current_version_id;
    if (sourceVersionId) {
      const sourceDetails = await client.query(
        `SELECT * FROM ${dTable} WHERE bom_id = $1 AND version_id = $2 ORDER BY parent_detail_id NULLS FIRST, id`,
        [bomId, sourceVersionId],
      );

      // old ID → new ID 매핑 (parent_detail_id 유지)
      const oldToNew: Record<string, string> = {};
      for (const d of sourceDetails.rows) {
        const insertResult = await client.query(
          `INSERT INTO ${dTable} (bom_id, version_id, parent_detail_id, child_item_id, quantity, unit, process_type, loss_rate, remark, level, base_qty, revision, seq_no, writer, company_code)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id`,
          [
            bomId,
            newVersionId,
            d.parent_detail_id ? (oldToNew[d.parent_detail_id] || null) : null,
            d.child_item_id,
            d.quantity,
            d.unit,
            d.process_type,
            d.loss_rate,
            d.remark,
            d.level,
            d.base_qty,
            d.revision,
            d.seq_no,
            d.writer,
            companyCode,
          ],
        );
        oldToNew[d.id] = insertResult.rows[0].id;
      }

      logger.info("BOM 버전 생성 - 디테일 복사 완료", {
        bomId, versionName, sourceVersionId, copiedCount: sourceDetails.rows.length,
      });
    }

    // BOM 헤더의 version과 current_version_id 갱신
    await client.query(
      `UPDATE bom SET version = $1, current_version_id = $2 WHERE id = $3`,
      [versionName, newVersionId, bomId],
    );

    logger.info("BOM 버전 생성 완료", { bomId, versionName, newVersionId, companyCode });
    return newVersion.rows[0];
  });
}

/**
 * 버전 불러오기: bom_detail 삭제/복원 없이 current_version_id만 전환
 */
export async function loadBomVersion(
  bomId: string, versionId: string, companyCode: string,
  versionTableName?: string, _detailTableName?: string,
) {
  const vTable = safeTableName(versionTableName || "", "bom_version");

  return transaction(async (client) => {
    const verRow = await client.query(
      `SELECT * FROM ${vTable} WHERE id = $1 AND bom_id = $2`,
      [versionId, bomId],
    );
    if (verRow.rows.length === 0) throw new Error("버전을 찾을 수 없습니다");

    const versionName = verRow.rows[0].version_name;

    // BOM 헤더의 version과 current_version_id만 전환
    await client.query(
      `UPDATE bom SET version = $1, current_version_id = $2 WHERE id = $3`,
      [versionName, versionId, bomId],
    );

    logger.info("BOM 버전 불러오기 완료", { bomId, versionId, versionName });
    return { restored: true, versionName };
  });
}

/**
 * 사용 확정: 선택 버전을 active로 변경 + current_version_id 갱신
 */
export async function activateBomVersion(bomId: string, versionId: string, tableName?: string) {
  const table = safeTableName(tableName || "", "bom_version");

  return transaction(async (client) => {
    const verRow = await client.query(
      `SELECT version_name FROM ${table} WHERE id = $1 AND bom_id = $2`,
      [versionId, bomId],
    );
    if (verRow.rows.length === 0) throw new Error("버전을 찾을 수 없습니다");

    // 기존 active -> inactive
    await client.query(
      `UPDATE ${table} SET status = 'inactive' WHERE bom_id = $1 AND status = 'active'`,
      [bomId],
    );
    // 선택한 버전 -> active
    await client.query(
      `UPDATE ${table} SET status = 'active' WHERE id = $1`,
      [versionId],
    );
    // BOM 헤더 갱신
    const versionName = verRow.rows[0].version_name;
    await client.query(
      `UPDATE bom SET version = $1, current_version_id = $2 WHERE id = $3`,
      [versionName, versionId, bomId],
    );

    logger.info("BOM 버전 사용 확정", { bomId, versionId, versionName });
    return { activated: true, versionName };
  });
}

/**
 * 신규 BOM 초기화: 첫 번째 버전 자동 생성 + version_id null인 디테일 보정
 * BOM 헤더의 version 필드를 그대로 버전명으로 사용 (사용자 입력값 존중)
 */
export async function initializeBomVersion(
  bomId: string, companyCode: string, createdBy: string,
) {
  return transaction(async (client) => {
    const bomRow = await client.query(`SELECT * FROM bom WHERE id = $1`, [bomId]);
    if (bomRow.rows.length === 0) throw new Error("BOM을 찾을 수 없습니다");
    const bomData = bomRow.rows[0];

    if (bomData.current_version_id) {
      await client.query(
        `UPDATE bom_detail SET version_id = $1 WHERE bom_id = $2 AND version_id IS NULL`,
        [bomData.current_version_id, bomId],
      );
      return { versionId: bomData.current_version_id, created: false };
    }

    // 이미 버전 레코드가 존재하는지 확인 (동시 호출 방지)
    const existingVersion = await client.query(
      `SELECT id, version_name FROM bom_version WHERE bom_id = $1 ORDER BY created_date ASC LIMIT 1`,
      [bomId],
    );
    if (existingVersion.rows.length > 0) {
      const existId = existingVersion.rows[0].id;
      await client.query(
        `UPDATE bom_detail SET version_id = $1 WHERE bom_id = $2 AND version_id IS NULL`,
        [existId, bomId],
      );
      await client.query(
        `UPDATE bom SET current_version_id = $1 WHERE id = $2 AND current_version_id IS NULL`,
        [existId, bomId],
      );
      return { versionId: existId, created: false };
    }

    const versionName = bomData.version || "1.0";

    const versionResult = await client.query(
      `INSERT INTO bom_version (bom_id, version_name, revision, status, created_by, company_code)
       VALUES ($1, $2, 0, 'active', $3, $4) RETURNING id`,
      [bomId, versionName, createdBy, companyCode],
    );
    const versionId = versionResult.rows[0].id;

    const updated = await client.query(
      `UPDATE bom_detail SET version_id = $1 WHERE bom_id = $2 AND version_id IS NULL`,
      [versionId, bomId],
    );

    await client.query(
      `UPDATE bom SET current_version_id = $1 WHERE id = $2`,
      [versionId, bomId],
    );

    logger.info("BOM 초기 버전 생성", { bomId, versionId, versionName, updatedDetails: updated.rowCount });
    return { versionId, versionName, created: true };
  });
}

// ─── BOM 엑셀 업로드 ─────────────────────────────

interface BomExcelRow {
  level: number;
  item_number: string;
  item_name?: string;
  quantity: number;
  unit?: string;
  process_type?: string;
  remark?: string;
}

interface BomExcelUploadResult {
  success: boolean;
  insertedCount: number;
  skippedCount: number;
  errors: string[];
  unmatchedItems: string[];
  createdBomId?: string;
}

/**
 * BOM 엑셀 업로드 - 새 BOM 생성
 * 
 * 엑셀 레벨 체계:
 *   레벨 0 = BOM 마스터 (최상위 품목) → bom 테이블에 INSERT
 *   레벨 1 = 직접 자품목 → bom_detail (parent_detail_id=null, DB level=0)
 *   레벨 2 = 자품목의 자품목 → bom_detail (parent_detail_id=부모ID, DB level=1)
 *   레벨 N = ... → bom_detail (DB level=N-1)
 */
export async function createBomFromExcel(
  companyCode: string,
  userId: string,
  rows: BomExcelRow[],
): Promise<BomExcelUploadResult> {
  const result: BomExcelUploadResult = {
    success: false,
    insertedCount: 0,
    skippedCount: 0,
    errors: [],
    unmatchedItems: [],
  };

  if (!rows || rows.length === 0) {
    result.errors.push("업로드할 데이터가 없습니다");
    return result;
  }

  const headerRow = rows.find(r => r.level === 0);
  const detailRows = rows.filter(r => r.level > 0);

  if (!headerRow) {
    result.errors.push("레벨 0(BOM 마스터) 행이 필요합니다");
    return result;
  }
  if (!headerRow.item_number?.trim()) {
    result.errors.push("레벨 0(BOM 마스터)의 품번은 필수입니다");
    return result;
  }
  if (detailRows.length === 0) {
    result.errors.push("하위품목이 없습니다 (레벨 1 이상의 행이 필요합니다)");
    return result;
  }

  // 레벨 유효성 검사
  for (let i = 0; i < rows.length; i++) {
    const row = rows[i];
    if (row.level < 0) {
      result.errors.push(`${i + 1}행: 레벨은 0 이상이어야 합니다`);
    }
    if (i > 0 && row.level > rows[i - 1].level + 1) {
      result.errors.push(`${i + 1}행: 레벨이 이전 행보다 2 이상 깊어질 수 없습니다 (현재: ${row.level}, 이전: ${rows[i - 1].level})`);
    }
    if (row.level > 0 && !row.item_number?.trim()) {
      result.errors.push(`${i + 1}행: 품번은 필수입니다`);
    }
  }

  if (result.errors.length > 0) {
    return result;
  }

  return transaction(async (client) => {
    // 1. 모든 품번 일괄 조회 (헤더 + 디테일)
    const allItemNumbers = [...new Set(rows.filter(r => r.item_number?.trim()).map(r => r.item_number.trim()))];
    const itemLookup = await client.query(
      `SELECT id, item_number, item_name, unit FROM item_info
       WHERE company_code = $1 AND item_number = ANY($2::text[])`,
      [companyCode, allItemNumbers],
    );

    const itemMap = new Map<string, { id: string; item_name: string; unit: string }>();
    for (const item of itemLookup.rows) {
      itemMap.set(item.item_number, { id: item.id, item_name: item.item_name, unit: item.unit });
    }

    for (const num of allItemNumbers) {
      if (!itemMap.has(num)) {
        result.unmatchedItems.push(num);
      }
    }
    if (result.unmatchedItems.length > 0) {
      result.errors.push(`매칭되지 않는 품번이 있습니다: ${result.unmatchedItems.join(", ")}`);
      return result;
    }

    // 2. bom 마스터 생성 (레벨 0)
    const headerItemInfo = itemMap.get(headerRow.item_number.trim())!;

    // 동일 품목으로 이미 BOM이 존재하는지 확인
    const dupCheck = await client.query(
      `SELECT id FROM bom WHERE item_id = $1 AND company_code = $2 AND status = 'active'`,
      [headerItemInfo.id, companyCode],
    );
    if (dupCheck.rows.length > 0) {
      result.errors.push(`해당 품목(${headerRow.item_number})으로 등록된 BOM이 이미 존재합니다`);
      return result;
    }

    const bomInsert = await client.query(
      `INSERT INTO bom (item_id, item_code, item_name, base_qty, unit, version, status, remark, writer, company_code)
       VALUES ($1, $2, $3, $4, $5, '1.0', 'active', $6, $7, $8)
       RETURNING id`,
      [
        headerItemInfo.id,
        headerRow.item_number.trim(),
        headerItemInfo.item_name,
        String(headerRow.quantity || 1),
        headerRow.unit || headerItemInfo.unit || null,
        headerRow.remark || null,
        userId,
        companyCode,
      ],
    );
    const newBomId = bomInsert.rows[0].id;
    result.createdBomId = newBomId;

    // 3. bom_version 생성
    const versionInsert = await client.query(
      `INSERT INTO bom_version (bom_id, version_name, revision, status, created_by, company_code)
       VALUES ($1, '1.0', 0, 'active', $2, $3) RETURNING id`,
      [newBomId, userId, companyCode],
    );
    const versionId = versionInsert.rows[0].id;

    await client.query(
      `UPDATE bom SET current_version_id = $1 WHERE id = $2`,
      [versionId, newBomId],
    );

    // 4. bom_detail INSERT (레벨 1+ → DB level = 엑셀 level - 1)
    const levelStack: string[] = [];
    const seqCounterByParent = new Map<string, number>();

    for (let i = 0; i < detailRows.length; i++) {
      const row = detailRows[i];
      const itemInfo = itemMap.get(row.item_number.trim())!;
      const dbLevel = row.level - 1;

      while (levelStack.length > dbLevel) {
        levelStack.pop();
      }

      const parentDetailId = levelStack.length > 0 ? levelStack[levelStack.length - 1] : null;
      const parentKey = parentDetailId || "__root__";
      const currentSeq = (seqCounterByParent.get(parentKey) || 0) + 1;
      seqCounterByParent.set(parentKey, currentSeq);

      const insertResult = await client.query(
        `INSERT INTO bom_detail (bom_id, version_id, parent_detail_id, child_item_id, level, seq_no, quantity, unit, loss_rate, process_type, remark, writer, company_code)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, '0', $9, $10, $11, $12)
         RETURNING id`,
        [
          newBomId,
          versionId,
          parentDetailId,
          itemInfo.id,
          String(dbLevel),
          String(currentSeq),
          String(row.quantity || 1),
          row.unit || itemInfo.unit || null,
          row.process_type || null,
          row.remark || null,
          userId,
          companyCode,
        ],
      );

      levelStack.push(insertResult.rows[0].id);
      result.insertedCount++;
    }

    // 5. 이력 기록
    await client.query(
      `INSERT INTO bom_history (bom_id, change_type, change_description, changed_by, company_code)
       VALUES ($1, 'excel_upload', $2, $3, $4)`,
      [newBomId, `엑셀 업로드로 BOM 생성 (하위품목 ${result.insertedCount}건)`, userId, companyCode],
    );

    result.success = true;
    logger.info("BOM 엑셀 업로드 - 새 BOM 생성 완료", {
      newBomId, companyCode,
      insertedCount: result.insertedCount,
    });

    return result;
  });
}

/**
 * BOM 엑셀 업로드 - 기존 BOM에 새 버전 생성
 * 
 * 엑셀에 레벨 0 행이 있으면 건너뛰고 (마스터는 이미 존재)
 * 레벨 1 이상만 bom_detail로 INSERT, 새 bom_version에 연결
 */
export async function createBomVersionFromExcel(
  bomId: string,
  companyCode: string,
  userId: string,
  rows: BomExcelRow[],
  versionName?: string,
): Promise<BomExcelUploadResult> {
  const result: BomExcelUploadResult = {
    success: false,
    insertedCount: 0,
    skippedCount: 0,
    errors: [],
    unmatchedItems: [],
  };

  if (!rows || rows.length === 0) {
    result.errors.push("업로드할 데이터가 없습니다");
    return result;
  }

  const detailRows = rows.filter(r => r.level > 0);
  result.skippedCount = rows.length - detailRows.length;

  if (detailRows.length === 0) {
    result.errors.push("하위품목이 없습니다 (레벨 1 이상의 행이 필요합니다)");
    return result;
  }

  // 레벨 유효성 검사
  for (let i = 0; i < rows.length; i++) {
    const row = rows[i];
    if (row.level < 0) {
      result.errors.push(`${i + 1}행: 레벨은 0 이상이어야 합니다`);
    }
    if (i > 0 && row.level > rows[i - 1].level + 1) {
      result.errors.push(`${i + 1}행: 레벨이 이전 행보다 2 이상 깊어질 수 없습니다`);
    }
    if (row.level > 0 && !row.item_number?.trim()) {
      result.errors.push(`${i + 1}행: 품번은 필수입니다`);
    }
  }

  if (result.errors.length > 0) {
    return result;
  }

  return transaction(async (client) => {
    // 1. BOM 존재 확인
    const bomRow = await client.query(
      `SELECT id, version FROM bom WHERE id = $1 AND company_code = $2`,
      [bomId, companyCode],
    );
    if (bomRow.rows.length === 0) {
      result.errors.push("BOM을 찾을 수 없습니다");
      return result;
    }

    // 2. 품번 → item_info 매핑
    const uniqueItemNumbers = [...new Set(detailRows.map(r => r.item_number.trim()))];
    const itemLookup = await client.query(
      `SELECT id, item_number, item_name, unit FROM item_info
       WHERE company_code = $1 AND item_number = ANY($2::text[])`,
      [companyCode, uniqueItemNumbers],
    );

    const itemMap = new Map<string, { id: string; item_name: string; unit: string }>();
    for (const item of itemLookup.rows) {
      itemMap.set(item.item_number, { id: item.id, item_name: item.item_name, unit: item.unit });
    }

    for (const num of uniqueItemNumbers) {
      if (!itemMap.has(num)) {
        result.unmatchedItems.push(num);
      }
    }
    if (result.unmatchedItems.length > 0) {
      result.errors.push(`매칭되지 않는 품번이 있습니다: ${result.unmatchedItems.join(", ")}`);
      return result;
    }

    // 3. 버전명 결정 (미입력 시 자동 채번)
    let finalVersionName = versionName?.trim();
    if (!finalVersionName) {
      const countResult = await client.query(
        `SELECT COUNT(*)::int as cnt FROM bom_version WHERE bom_id = $1`,
        [bomId],
      );
      finalVersionName = `${(countResult.rows[0].cnt || 0) + 1}.0`;
    }

    // 중복 체크
    const dupCheck = await client.query(
      `SELECT id FROM bom_version WHERE bom_id = $1 AND version_name = $2`,
      [bomId, finalVersionName],
    );
    if (dupCheck.rows.length > 0) {
      result.errors.push(`이미 존재하는 버전명입니다: ${finalVersionName}`);
      return result;
    }

    // 4. bom_version 생성
    const versionInsert = await client.query(
      `INSERT INTO bom_version (bom_id, version_name, revision, status, created_by, company_code)
       VALUES ($1, $2, 0, 'developing', $3, $4) RETURNING id`,
      [bomId, finalVersionName, userId, companyCode],
    );
    const newVersionId = versionInsert.rows[0].id;

    // 5. bom_detail INSERT
    const levelStack: string[] = [];
    const seqCounterByParent = new Map<string, number>();

    for (let i = 0; i < detailRows.length; i++) {
      const row = detailRows[i];
      const itemInfo = itemMap.get(row.item_number.trim())!;
      const dbLevel = row.level - 1;

      while (levelStack.length > dbLevel) {
        levelStack.pop();
      }

      const parentDetailId = levelStack.length > 0 ? levelStack[levelStack.length - 1] : null;
      const parentKey = parentDetailId || "__root__";
      const currentSeq = (seqCounterByParent.get(parentKey) || 0) + 1;
      seqCounterByParent.set(parentKey, currentSeq);

      const insertResult = await client.query(
        `INSERT INTO bom_detail (bom_id, version_id, parent_detail_id, child_item_id, level, seq_no, quantity, unit, loss_rate, process_type, remark, writer, company_code)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, '0', $9, $10, $11, $12)
         RETURNING id`,
        [
          bomId,
          newVersionId,
          parentDetailId,
          itemInfo.id,
          String(dbLevel),
          String(currentSeq),
          String(row.quantity || 1),
          row.unit || itemInfo.unit || null,
          row.process_type || null,
          row.remark || null,
          userId,
          companyCode,
        ],
      );

      levelStack.push(insertResult.rows[0].id);
      result.insertedCount++;
    }

    // 6. BOM 헤더의 version과 current_version_id 갱신
    await client.query(
      `UPDATE bom SET version = $1, current_version_id = $2 WHERE id = $3`,
      [finalVersionName, newVersionId, bomId],
    );

    // 7. 이력 기록
    await client.query(
      `INSERT INTO bom_history (bom_id, change_type, change_description, changed_by, company_code)
       VALUES ($1, 'excel_upload', $2, $3, $4)`,
      [bomId, `엑셀 업로드로 새 버전 ${finalVersionName} 생성 (하위품목 ${result.insertedCount}건)`, userId, companyCode],
    );

    result.success = true;
    result.createdBomId = bomId;
    logger.info("BOM 엑셀 업로드 - 새 버전 생성 완료", {
      bomId, companyCode, versionName: finalVersionName,
      insertedCount: result.insertedCount,
    });

    return result;
  });
}

/**
 * BOM 엑셀 다운로드용 데이터 조회
 * 
 * 화면과 동일한 레벨 체계로 출력:
 *   레벨 0 = BOM 헤더 (최상위 품목)
 *   레벨 1 = 직접 자품목 (DB level=0)
 *   레벨 N = DB level N-1
 * 
 * DFS로 순회하여 부모-자식 순서 보장
 */
export async function downloadBomExcelData(
  bomId: string,
  companyCode: string,
): Promise<Record<string, any>[]> {
  // BOM 헤더 정보 조회 (최상위 품목)
  const bomHeader = await queryOne<Record<string, any>>(
    `SELECT b.*, ii.item_number, ii.item_name, ii.division, ii.unit as item_unit
     FROM bom b
     LEFT JOIN item_info ii ON b.item_id = ii.id
     WHERE b.id = $1 AND b.company_code = $2`,
    [bomId, companyCode],
  );

  if (!bomHeader) return [];

  const flatList: Record<string, any>[] = [];

  // 레벨 0: BOM 헤더 (최상위 품목)
  flatList.push({
    level: 0,
    item_number: bomHeader.item_number || "",
    item_name: bomHeader.item_name || "",
    quantity: bomHeader.base_qty || "1",
    unit: bomHeader.item_unit || bomHeader.unit || "",
    process_type: "",
    remark: bomHeader.remark || "",
    _is_header: true,
  });

  // 하위 품목 조회
  const versionId = bomHeader.current_version_id;
  const whereVersion = versionId ? `AND bd.version_id = $3` : `AND bd.version_id IS NULL`;
  const params = versionId ? [bomId, companyCode, versionId] : [bomId, companyCode];

  const details = await query(
    `SELECT bd.*, ii.item_number, ii.item_name, ii.division, ii.unit as item_unit, ii.size, ii.material
     FROM bom_detail bd
     LEFT JOIN item_info ii ON bd.child_item_id = ii.id
     WHERE bd.bom_id = $1 AND bd.company_code = $2 ${whereVersion}
     ORDER BY bd.parent_detail_id NULLS FIRST, bd.seq_no::int`,
    params,
  );

  // 부모 ID별 자식 목록으로 맵 구성
  const childrenMap = new Map<string, any[]>();
  const roots: any[] = [];
  for (const d of details) {
    if (!d.parent_detail_id) {
      roots.push(d);
    } else {
      if (!childrenMap.has(d.parent_detail_id)) childrenMap.set(d.parent_detail_id, []);
      childrenMap.get(d.parent_detail_id)!.push(d);
    }
  }

  // DFS: depth로 정확한 레벨 계산 (DB level 무시, 실제 트리 깊이 사용)
  const dfs = (nodes: any[], depth: number) => {
    for (const node of nodes) {
      flatList.push({
        level: depth,
        item_number: node.item_number || "",
        item_name: node.item_name || "",
        quantity: node.quantity || "1",
        unit: node.unit || node.item_unit || "",
        process_type: node.process_type || "",
        remark: node.remark || "",
      });
      const children = childrenMap.get(node.id) || [];
      if (children.length > 0) {
        dfs(children, depth + 1);
      }
    }
  };

  // 루트 노드들은 레벨 1 (BOM 헤더가 0이므로)
  dfs(roots, 1);

  return flatList;
}

/**
 * 버전 삭제: 해당 version_id의 bom_detail 행도 함께 삭제
 */
export async function deleteBomVersion(
  bomId: string, versionId: string,
  tableName?: string, detailTableName?: string,
) {
  const table = safeTableName(tableName || "", "bom_version");
  const dTable = safeTableName(detailTableName || "", "bom_detail");

  return transaction(async (client) => {
    // active 상태 버전은 삭제 불가
    const checkResult = await client.query(
      `SELECT status FROM ${table} WHERE id = $1 AND bom_id = $2`,
      [versionId, bomId],
    );
    if (checkResult.rows.length === 0) throw new Error("버전을 찾을 수 없습니다");
    if (checkResult.rows[0].status === "active") {
      throw new Error("사용중인 버전은 삭제할 수 없습니다");
    }

    // 해당 버전의 bom_detail 행 삭제
    const deleteDetails = await client.query(
      `DELETE FROM ${dTable} WHERE bom_id = $1 AND version_id = $2`,
      [bomId, versionId],
    );

    // 버전 레코드 삭제
    const deleteVersion = await client.query(
      `DELETE FROM ${table} WHERE id = $1 AND bom_id = $2 RETURNING id`,
      [versionId, bomId],
    );

    logger.info("BOM 버전 삭제", {
      bomId, versionId,
      deletedDetails: deleteDetails.rowCount,
    });

    return deleteVersion.rows.length > 0;
  });
}
