// ============================================================
// 생산관리 > M-BOM 관리 — wace productionplanning.xml 1:1 이식.
//
// 매퍼 매핑 (원본: wace_plm/src/com/pms/mapper/productionplanning.xml,
//                wace_plm/src/com/pms/mapper/partMng.xml):
//   mBomMgmtGridList            → list()                  (PR-A1, ~3119)
//   getProjectMgmtDetail        → getDetail()             (PR-A2, ~3218)
//   getLatestMbomByProjectId    → getLatestSavedMbom()    (PR-A2, ~3570)
//   getLatestMbomTemplateByPartNo → getLatestTemplate()   (PR-A2, ~3591)
//   getSavedMbomTreeList        → getSavedTree()          (PR-A2, ~4359)
//   getMbomStructureOnly        → getStructureOnly()      (PR-A2, ~4538)
//   getMbomTemplateDetails      → getTemplateDetails()    (PR-A2, ~3794)
//   partMng.getBOMTreeList      → getEbomWorkingTree()    (PR-A2, partMng.xml ~3549)
//
// 트리 분기 (mBomPopupLeft.do 1:1):
//   1) SAVED         — mbom_header 에 status='Y' 가 있으면 그 트리
//   2) ASSIGNED_EBOM — project_mgmt.source_bom_type='EBOM' + source_ebom_objid
//   3) ASSIGNED_MBOM — project_mgmt.source_bom_type='MBOM' + source_mbom_objid
//   4) TEMPLATE      — Machine 이외(product != '0000928') + part_no 있으면 동일 품번 mbom_header
//   5) NONE          — 빈 트리
// ============================================================

import { getPool } from "../database/db";
import { createObjId } from "../utils/objidUtil";

// ─── 필터/페이지 타입 ──────────────────────────────────────────

export interface MbomListFilter {
  search_category_cd?: string;
  search_product_cd?: string;
  search_area_cd?: string;
  search_customer_objid?: string;
  search_paid_type?: string;
  search_serial_no?: string;
  search_part_no?: string;
  search_part_name?: string;
  search_receipt_date_from?: string;
  search_receipt_date_to?: string;
  search_req_del_date_from?: string;
  search_req_del_date_to?: string;
  page?: number;
  page_size?: number;
}

function paginate(filter: { page?: number; page_size?: number }) {
  const page = Math.max(1, Number(filter.page) || 1);
  const pageSize = Math.min(500, Math.max(1, Number(filter.page_size) || 50));
  return { limit: pageSize, offset: (page - 1) * pageSize, page, pageSize };
}

// ─── WHERE 절 빌더 (매퍼 mBomMgmtGridList <if> 조건 1:1) ──────

function buildWhere(filter: MbomListFilter, startIdx: number) {
  const params: any[] = [];
  const conds: string[] = [];
  let idx = startIdx;

  if (filter.search_category_cd) {
    conds.push(`CM.CATEGORY_CD = $${idx++}`);
    params.push(filter.search_category_cd);
  }
  if (filter.search_product_cd) {
    conds.push(`CM.PRODUCT = $${idx++}`);
    params.push(filter.search_product_cd);
  }
  if (filter.search_area_cd) {
    // 운영판: CODE_NAME(CM.AREA_CD) = '국내'/'해외' (사람이 보는 값으로 검색)
    conds.push(`(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.AREA_CD LIMIT 1) = $${idx++}`);
    params.push(filter.search_area_cd);
  }
  if (filter.search_receipt_date_from) {
    conds.push(`PM.REGDATE >= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
    params.push(filter.search_receipt_date_from);
  }
  if (filter.search_receipt_date_to) {
    conds.push(`PM.REGDATE < TO_DATE($${idx++}, 'YYYY-MM-DD') + INTERVAL '1 day'`);
    params.push(filter.search_receipt_date_to);
  }
  if (filter.search_customer_objid) {
    // 운영판 3-way 매칭 (C_xxx 양방향) 1:1
    conds.push(
      `(CM.CUSTOMER_OBJID = $${idx} OR CM.CUSTOMER_OBJID = REPLACE($${idx}, 'C_', '') OR 'C_' || CM.CUSTOMER_OBJID = $${idx})`
    );
    params.push(filter.search_customer_objid);
    idx++;
  }
  if (filter.search_paid_type) {
    conds.push(`CM.PAID_TYPE = $${idx++}`);
    params.push(filter.search_paid_type);
  }
  if (filter.search_serial_no) {
    conds.push(
      `EXISTS (SELECT 1 FROM CONTRACT_ITEM_SERIAL CIS
               WHERE CIS.ITEM_OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID
                 AND UPPER(CIS.STATUS) = 'ACTIVE'
                 AND UPPER(CIS.SERIAL_NO) LIKE '%' || UPPER($${idx++}) || '%')`
    );
    params.push(filter.search_serial_no);
  }
  if (filter.search_req_del_date_from) {
    conds.push(`COALESCE(CI.DUE_DATE, PM.DUE_DATE, CM.REQ_DEL_DATE) >= $${idx++}`);
    params.push(filter.search_req_del_date_from);
  }
  if (filter.search_req_del_date_to) {
    conds.push(`COALESCE(CI.DUE_DATE, PM.DUE_DATE, CM.REQ_DEL_DATE) <= $${idx++}`);
    params.push(filter.search_req_del_date_to);
  }
  if (filter.search_part_no) {
    conds.push(
      `(UPPER(PM.PART_NO) LIKE '%' || UPPER($${idx}) || '%' OR UPPER(CI.PART_NO) LIKE '%' || UPPER($${idx}) || '%')`
    );
    params.push(filter.search_part_no);
    idx++;
  }
  if (filter.search_part_name) {
    conds.push(
      `(UPPER(PM.PART_NAME) LIKE '%' || UPPER($${idx}) || '%' OR UPPER(CI.PART_NAME) LIKE '%' || UPPER($${idx}) || '%')`
    );
    params.push(filter.search_part_name);
    idx++;
  }

  return { sql: conds.length ? "AND " + conds.join(" AND ") : "", params };
}

// ─── M-BOM 관리 그리드 ──────────────────────────────────────────
//
// 매퍼 productionplanning.xml mBomMgmtGridList (라인 2874~3119) 1:1 이식.

export async function list(filter: MbomListFilter) {
  const { limit, offset, page, pageSize } = paginate(filter);
  const where = buildWhere(filter, 1);
  const pool = getPool();

  const baseSql = `
    FROM PROJECT_MGMT PM
    LEFT JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
    LEFT OUTER JOIN CONTRACT_ITEM CI ON (
      CASE
        WHEN PM.CONTRACT_ITEM_OBJID IS NOT NULL THEN CI.OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID
        ELSE CI.CONTRACT_OBJID = PM.CONTRACT_OBJID AND CI.PART_OBJID = PM.PART_OBJID
      END
    )
      AND CI.STATUS = 'ACTIVE'
    WHERE 1=1
      AND PM.PROJECT_NO IS NOT NULL
      AND PM.PROJECT_NO != ''
      ${where.sql}
  `;

  const dataSql = `
    SELECT
      PM.OBJID,
      PM.CONTRACT_OBJID,
      PM.PROJECT_NO,
      CM.CATEGORY_CD,
      COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.CATEGORY_CD LIMIT 1), '') AS CATEGORY_NAME,
      CM.PRODUCT,
      COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.PRODUCT LIMIT 1), '') AS PRODUCT_NAME,
      CM.AREA_CD,
      COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.AREA_CD LIMIT 1), '') AS AREA_NAME,
      TO_CHAR(PM.REGDATE, 'YYYY-MM-DD') AS RECEIPT_DATE,
      (SELECT user_name(MH.WRITER)
         FROM MBOM_HEADER MH
        WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR
          AND MH.STATUS = 'Y'
        ORDER BY MH.REGDATE DESC
        LIMIT 1) AS WRITER_NAME,
      CM.CUSTOMER_OBJID,
      COALESCE(
        CASE WHEN CM.CUSTOMER_OBJID LIKE 'C_%'
             THEN (SELECT CLIENT_NM FROM CLIENT_MNG AS C
                    WHERE 'C_' || C.OBJID::VARCHAR = CM.CUSTOMER_OBJID LIMIT 1)
             ELSE (SELECT SUPPLY_NAME FROM SUPPLY_MNG
                    WHERE OBJID::VARCHAR = CM.CUSTOMER_OBJID::VARCHAR LIMIT 1) END,
        ''
      ) AS CUSTOMER_NAME,
      CM.PAID_TYPE,
      COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.PAID_TYPE LIMIT 1),
        CASE
          WHEN CM.PAID_TYPE = 'paid' THEN '유상'
          WHEN CM.PAID_TYPE = 'free' THEN '무상'
          ELSE '' END
      ) AS PAID_TYPE_NAME,
      -- 품목 정보: CONTRACT_ITEM 우선, 없으면 PM
      COALESCE(CI.PART_NO, PM.PART_NO, '')     AS PART_NO,
      COALESCE(CI.PART_NAME, PM.PART_NAME, '') AS PART_NAME,
      CI.PART_OBJID,
      -- S/N 표시: "첫S/N 외 N건"
      (SELECT
         CASE
           WHEN COUNT(*) = 0 THEN ''
           WHEN COUNT(*) = 1 THEN MIN(CIS.SERIAL_NO)
           ELSE MIN(CIS.SERIAL_NO) || ' 외 ' || (COUNT(*) - 1)::TEXT || '건' END
         FROM CONTRACT_ITEM_SERIAL CIS
         WHERE CIS.ITEM_OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID
           AND UPPER(CIS.STATUS) = 'ACTIVE'
           AND CIS.SERIAL_NO IS NOT NULL
           AND CIS.SERIAL_NO != ''
      ) AS SERIAL_NO,
      -- S/N 전체 (콤마 리스트) — 팝업용
      (SELECT STRING_AGG(CIS.SERIAL_NO, ', ' ORDER BY CIS.SERIAL_NO)
         FROM CONTRACT_ITEM_SERIAL CIS
         WHERE CIS.ITEM_OBJID::VARCHAR = PM.CONTRACT_ITEM_OBJID
           AND UPPER(CIS.STATUS) = 'ACTIVE'
           AND CIS.SERIAL_NO IS NOT NULL
           AND CIS.SERIAL_NO != ''
      ) AS SERIAL_NO_LIST,
      COALESCE(NULLIF(PM.QUANTITY, '')::numeric, NULLIF(CI.ORDER_QUANTITY, '')::numeric, 0) AS QUANTITY,
      COALESCE(CI.DUE_DATE, PM.DUE_DATE, CM.REQ_DEL_DATE) AS REQ_DEL_DATE,
      COALESCE(CI.CUSTOMER_REQUEST, '') AS CUSTOMER_REQUEST,
      -- E-BOM 정보
      COALESCE(CI.PART_OBJID, PM.PART_OBJID) AS BOM_REPORT_OBJID,
      COALESCE((SELECT PBR.STATUS FROM PART_BOM_REPORT PBR
                 WHERE PBR.OBJID::VARCHAR = COALESCE(CI.PART_OBJID, PM.PART_OBJID) LIMIT 1), '') AS EBOM_STATUS,
      COALESCE((SELECT TO_CHAR(PBR.REGDATE, 'YYYY-MM-DD') FROM PART_BOM_REPORT PBR
                 WHERE PBR.OBJID::VARCHAR = COALESCE(CI.PART_OBJID, PM.PART_OBJID) LIMIT 1), '') AS EBOM_REGDATE,
      -- M-BOM HEADER OBJID
      (SELECT MH.OBJID::VARCHAR FROM MBOM_HEADER MH
        WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
        ORDER BY MH.REGDATE DESC LIMIT 1) AS MBOM_HEADER_OBJID,
      -- 구매리스트 OBJID + 생성일
      (SELECT SRM.OBJID::VARCHAR FROM SALES_REQUEST_MASTER SRM
        WHERE SRM.MBOM_HEADER_OBJID = (
          SELECT MH.OBJID::VARCHAR FROM MBOM_HEADER MH
           WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
           ORDER BY MH.REGDATE DESC LIMIT 1
        ) LIMIT 1) AS PURCHASE_LIST_OBJID,
      (SELECT TO_CHAR(SRM.REGDATE, 'YYYY-MM-DD') FROM SALES_REQUEST_MASTER SRM
        WHERE SRM.MBOM_HEADER_OBJID = (
          SELECT MH.OBJID::VARCHAR FROM MBOM_HEADER MH
           WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
           ORDER BY MH.REGDATE DESC LIMIT 1
        ) LIMIT 1) AS PURCHASE_LIST_DATE,
      -- M-BOM 상태 ('Y' 있으면 Y, 아니면 PM.mbom_status raw)
      COALESCE(
        (SELECT CASE WHEN COUNT(*) > 0 THEN 'Y' ELSE COALESCE(PM.MBOM_STATUS, '') END
           FROM MBOM_HEADER MH
          WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y' LIMIT 1),
        COALESCE(PM.MBOM_STATUS, '')
      ) AS MBOM_STATUS,
      -- M-BOM 품번
      COALESCE(
        (SELECT MH.MBOM_NO FROM MBOM_HEADER MH
          WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
          ORDER BY MH.REGDATE DESC LIMIT 1), ''
      ) AS MBOM_PART_NO,
      -- M-BOM 저장일 (수정일 우선, 없으면 등록일)
      (SELECT TO_CHAR(COALESCE(MH.EDIT_DATE, MH.REGDATE), 'YYYY-MM-DD')
         FROM MBOM_HEADER MH
        WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
        ORDER BY COALESCE(MH.EDIT_DATE, MH.REGDATE) DESC LIMIT 1) AS MBOM_REGDATE,
      -- M-BOM 작성자/수정자
      (SELECT user_name(COALESCE(MH.EDITER, MH.WRITER))
         FROM MBOM_HEADER MH
        WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
        ORDER BY COALESCE(MH.EDITER, MH.WRITER) DESC LIMIT 1) AS MBOM_EDITOR,
      -- M-BOM 변경이력 카운트 (0이면 NULL)
      NULLIF(
        (SELECT COUNT(1)::INTEGER FROM MBOM_HISTORY MHI
          WHERE MHI.MBOM_HEADER_OBJID = (
            SELECT MH.OBJID::VARCHAR FROM MBOM_HEADER MH
             WHERE MH.PROJECT_OBJID = PM.OBJID::VARCHAR AND MH.STATUS = 'Y'
             ORDER BY MH.REGDATE DESC LIMIT 1
          )), 0
      ) AS MBOM_VERSION
    ${baseSql}
    ORDER BY PM.REGDATE DESC, CI.PART_NO
    LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}
  `;

  const countSql = `SELECT COUNT(*)::int AS cnt ${baseSql}`;

  const dataParams = [...where.params, limit, offset];

  const [dataRes, countRes] = await Promise.all([
    pool.query(dataSql, dataParams),
    pool.query(countSql, where.params),
  ]);

  return {
    rows: dataRes.rows,
    totalCount: countRes.rows[0]?.cnt ?? 0,
    page,
    pageSize,
  };
}

// ─── 단건 상세 (getProjectMgmtDetail) ──────────────────────────
//
// 매퍼 productionplanning.xml getProjectMgmtDetail (라인 3150~3218) 1:1.
// TOTAL_PROD_QTY = production_plan 우선 → PM.QUANTITY fallback.

export async function getDetail(objid: string) {
  const pool = getPool();
  const sql = `
    SELECT
      PM.OBJID::VARCHAR              AS objid,
      PM.CONTRACT_OBJID              AS contract_objid,
      PM.PROJECT_NO                  AS project_no,
      PM.BOM_REPORT_OBJID            AS bom_report_objid,
      PM.PART_OBJID                  AS part_objid,
      PM.PART_NO                     AS part_no,
      PM.PART_NAME                   AS part_name,
      PM.SOURCE_BOM_TYPE             AS source_bom_type,
      PM.SOURCE_EBOM_OBJID           AS source_ebom_objid,
      PM.SOURCE_MBOM_OBJID           AS source_mbom_objid,
      PM.QUANTITY                    AS quantity,
      COALESCE(
        (SELECT NULLIF(PP.TOTAL_PROD_QTY, '')::numeric
           FROM PRODUCTION_PLAN PP
          WHERE PP.PROJECT_OBJID = PM.OBJID::VARCHAR
            AND UPPER(PP.STATUS) = 'ACTIVE'
          LIMIT 1),
        COALESCE(NULLIF(PM.QUANTITY, '')::numeric, 0)
      ) AS total_prod_qty,
      COALESCE(
        (SELECT PBR.PART_NO FROM PART_BOM_REPORT PBR
          WHERE PBR.OBJID::VARCHAR = PM.BOM_REPORT_OBJID
            AND PM.MBOM_STATUS = 'Y'
          LIMIT 1), ''
      ) AS mbom_part_no,
      CM.CATEGORY_CD                 AS category_cd,
      COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.CATEGORY_CD LIMIT 1), '') AS category_name,
      CM.PRODUCT                     AS product,
      CM.PRODUCT                     AS product_code,
      COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.PRODUCT LIMIT 1), '')     AS product_name,
      CM.AREA_CD                     AS area_cd,
      COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CM.AREA_CD LIMIT 1), '')     AS area_name,
      CM.CUSTOMER_OBJID              AS customer_objid,
      COALESCE(
        CASE WHEN CM.CUSTOMER_OBJID LIKE 'C_%'
             THEN (SELECT CLIENT_NM FROM CLIENT_MNG AS C
                    WHERE 'C_' || C.OBJID::VARCHAR = CM.CUSTOMER_OBJID LIMIT 1)
             ELSE (SELECT SUPPLY_NAME FROM SUPPLY_MNG
                    WHERE OBJID::VARCHAR = CM.CUSTOMER_OBJID::VARCHAR LIMIT 1) END,
        ''
      ) AS customer_name,
      CM.PAID_TYPE                   AS paid_type,
      CM.REQ_DEL_DATE                AS req_del_date,
      TO_CHAR(PM.REGDATE, 'YYYY-MM-DD') AS receipt_date,
      COALESCE(
        (SELECT TO_CHAR(PBR.REGDATE, 'YYYY-MM-DD')
           FROM PART_BOM_REPORT PBR
          WHERE PBR.OBJID::VARCHAR = PM.BOM_REPORT_OBJID
            AND PM.MBOM_STATUS = 'Y'
          LIMIT 1),
        TO_CHAR(PM.REGDATE, 'YYYY-MM-DD')
      ) AS mbom_regdate
    FROM PROJECT_MGMT PM
    INNER JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
   WHERE PM.OBJID::VARCHAR = $1
   LIMIT 1
  `;
  const r = await pool.query(sql, [objid]);
  return r.rows[0] ?? null;
}

// ─── 분기 진입점 (mBomPopupLeft.do 1:1) ────────────────────────
//
// 운영판 ProductionPlanningController:1113~1276 의 분기 로직:
//   1) 저장된 M-BOM (mbom_header.status='Y') 우선
//   2) 없으면 source_bom_type 으로 EBOM/MBOM 분기
//   3) 그래도 없으면 Machine 이외 + part_no 매칭으로 템플릿
//   4) 모두 없으면 빈 트리

export type BomDataType = "SAVED" | "ASSIGNED_EBOM" | "ASSIGNED_MBOM" | "TEMPLATE" | "NONE";

export interface MbomTreeResult {
  bom_data_type: BomDataType;
  bom_report_objid: string | null;
  max_level: number;
  rows: any[];
}

export async function getTree(objid: string): Promise<MbomTreeResult> {
  const detail = await getDetail(objid);
  if (!detail) {
    return { bom_data_type: "NONE", bom_report_objid: null, max_level: 1, rows: [] };
  }

  // 1) SAVED — getLatestMbomByProjectId
  const saved = await getLatestSavedMbom(objid);
  if (saved && saved.objid) {
    const rows = await getSavedTree(saved.objid);
    return finalize("SAVED", saved.objid, rows);
  }

  // 2) ASSIGNED_EBOM
  if (detail.source_bom_type === "EBOM" && detail.source_ebom_objid) {
    const rows = await getEbomWorkingTree(detail.source_ebom_objid);
    return finalize("ASSIGNED_EBOM", detail.source_ebom_objid, rows);
  }

  // 3) ASSIGNED_MBOM
  if (detail.source_bom_type === "MBOM" && detail.source_mbom_objid) {
    const rows = await getStructureOnly(detail.source_mbom_objid);
    return finalize("ASSIGNED_MBOM", detail.source_mbom_objid, rows);
  }

  // 4) TEMPLATE — Machine 이외 + part_no
  if (detail.product_code !== "0000928" && detail.part_no) {
    const tpl = await getLatestTemplate(detail.part_no);
    if (tpl && tpl.template_header_objid) {
      const rows = await getTemplateDetails(tpl.template_header_objid);
      return finalize("TEMPLATE", tpl.template_header_objid, rows);
    }
  }

  // 5) NONE
  return { bom_data_type: "NONE", bom_report_objid: detail.bom_report_objid ?? null, max_level: 1, rows: [] };
}

function finalize(type: BomDataType, bomReportObjid: string, rows: any[]): MbomTreeResult {
  let maxLevel = 1;
  for (const r of rows) {
    const lv = Number(r.level ?? r.LEVEL ?? 1);
    if (lv > maxLevel) maxLevel = lv;
  }
  return { bom_data_type: type, bom_report_objid: bomReportObjid, max_level: maxLevel, rows };
}

// ─── 분기 1) SAVED 진입 ──────────────────────────────────────
//
// 매퍼 getLatestMbomByProjectId (productionplanning.xml:3555~3570) 1:1.

async function getLatestSavedMbom(projectObjId: string) {
  const pool = getPool();
  const r = await pool.query(
    `SELECT OBJID::VARCHAR AS objid, MBOM_NO AS mbom_no,
            SOURCE_BOM_TYPE AS source_bom_type,
            SOURCE_EBOM_OBJID AS source_ebom_objid,
            SOURCE_MBOM_OBJID AS source_mbom_objid,
            PROJECT_OBJID AS project_objid, STATUS AS status, REGDATE AS regdate
       FROM MBOM_HEADER
      WHERE PROJECT_OBJID = $1 AND STATUS = 'Y'
      ORDER BY REGDATE DESC LIMIT 1`,
    [projectObjId],
  );
  return r.rows[0] ?? null;
}

// ─── 분기 4) TEMPLATE 진입 ───────────────────────────────────
//
// 매퍼 getLatestMbomTemplateByPartNo (productionplanning.xml:3573~3591) 1:1.

async function getLatestTemplate(partNo: string) {
  const pool = getPool();
  const r = await pool.query(
    `SELECT MH.OBJID::VARCHAR AS template_header_objid,
            MH.MBOM_NO         AS template_mbom_no,
            MH.PART_NO         AS part_no,
            MH.PART_NAME       AS part_name,
            MH.SOURCE_BOM_TYPE AS source_bom_type,
            MH.SOURCE_EBOM_OBJID AS source_ebom_objid,
            MH.SOURCE_MBOM_OBJID AS source_mbom_objid,
            TO_CHAR(MH.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS regdate
       FROM MBOM_HEADER MH
       INNER JOIN PROJECT_MGMT PM ON MH.PROJECT_OBJID = PM.OBJID::VARCHAR
       INNER JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
      WHERE MH.PART_NO = $1
        AND MH.STATUS = 'Y'
        AND CM.PRODUCT != '0000928'
      ORDER BY MH.REGDATE DESC LIMIT 1`,
    [partNo],
  );
  return r.rows[0] ?? null;
}

// ─── 분기 1-SAVED 트리 ───────────────────────────────────────
//
// 매퍼 getSavedMbomTreeList (productionplanning.xml:4114~4359) 1:1.
// RECURSIVE CTE + PART_MNG 조인 + ATTACH_FILE_INFO 카운트 (CU01/02/03_CNT) + 소재소요량.

async function getSavedTree(mbomHeaderObjid: string) {
  const pool = getPool();
  const r = await pool.query(SAVED_TREE_SQL, [mbomHeaderObjid]);
  return r.rows;
}

// ─── 분기 3-ASSIGNED_MBOM 구조만 ─────────────────────────────
//
// 매퍼 getMbomStructureOnly (productionplanning.xml:4362~4538) 1:1.
// 생산 정보는 NULL — 구조만 표시.

async function getStructureOnly(mbomHeaderObjid: string) {
  const pool = getPool();
  const r = await pool.query(STRUCTURE_ONLY_SQL, [mbomHeaderObjid]);
  return r.rows;
}

// ─── 분기 4-TEMPLATE 트리 ────────────────────────────────────
//
// 매퍼 getMbomTemplateDetails (productionplanning.xml:3594~3794) 1:1.
// ORDER_QTY/PRODUCTION_QTY 가 빠진 점만 SAVED 와 다름.

async function getTemplateDetails(mbomHeaderObjid: string) {
  const pool = getPool();
  const r = await pool.query(TEMPLATE_TREE_SQL, [mbomHeaderObjid]);
  return r.rows;
}

// ─── 분기 2-ASSIGNED_EBOM 트리 ───────────────────────────────
//
// 매퍼 partMng.getBOMTreeList (partMng.xml:3289~3549) - search_type='working' 1:1.
// bom_part_qty RECURSIVE CTE + PART_MNG 조인.

async function getEbomWorkingTree(bomReportObjid: string) {
  const pool = getPool();
  const r = await pool.query(EBOM_WORKING_TREE_SQL, [bomReportObjid]);
  return r.rows;
}

// ─── 트리 SELECT 본문 (매퍼 4종 1:1, lowercase alias) ──────

const SAVED_TREE_SQL = `
WITH RECURSIVE VIEW_BOM(
  MBOM_HEADER_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
  PART_OBJID, PART_NO, PART_NAME, QTY, ITEM_QTY, QTY_TEMP,
  REGDATE, SEQ, STATUS, LEV, PATH, PATH2, CYCLE,
  UNIT, SUPPLY_TYPE, MAKE_OR_BUY,
  RAW_MATERIAL_PART_NO, RAW_MATERIAL_SPEC, RAW_MATERIAL, RAW_MATERIAL_SIZE,
  PROCESSING_VENDOR, PROCESSING_DEADLINE, GRINDING_DEADLINE,
  REQUIRED_QTY, ORDER_QTY, PRODUCTION_QTY, STOCK_QTY, SHORTAGE_QTY,
  VENDOR, UNIT_PRICE, PROCESSING_UNIT_PRICE, TOTAL_PRICE, CURRENCY,
  LEAD_TIME, MIN_ORDER_QTY, WRITER, EDITER, EDIT_DATE, REMARK
) AS (
  SELECT A.MBOM_HEADER_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
         A.PART_OBJID, A.PART_NO, A.PART_NAME, A.QTY, A.ITEM_QTY, A.QTY,
         A.REGDATE, A.SEQ, A.STATUS, 1,
         ARRAY [A.CHILD_OBJID::TEXT],
         ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
         FALSE,
         A.UNIT, A.SUPPLY_TYPE, A.MAKE_OR_BUY,
         A.RAW_MATERIAL_PART_NO, A.RAW_MATERIAL_SPEC, A.RAW_MATERIAL, A.RAW_MATERIAL_SIZE,
         A.PROCESSING_VENDOR, A.PROCESSING_DEADLINE, A.GRINDING_DEADLINE,
         A.REQUIRED_QTY, A.ORDER_QTY, A.PRODUCTION_QTY, A.STOCK_QTY, A.SHORTAGE_QTY,
         A.VENDOR, A.UNIT_PRICE, A.PROCESSING_UNIT_PRICE, A.TOTAL_PRICE, A.CURRENCY,
         A.LEAD_TIME, A.MIN_ORDER_QTY, A.WRITER, A.EDITER, A.EDIT_DATE, A.REMARK
    FROM MBOM_DETAIL A
   WHERE 1=1
     AND (A.PARENT_OBJID IS NULL OR A.PARENT_OBJID = '')
     AND A.MBOM_HEADER_OBJID = $1
     AND A.STATUS = 'ACTIVE'
  UNION ALL
  SELECT B.MBOM_HEADER_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
         B.PART_OBJID, B.PART_NO, B.PART_NAME, B.QTY, B.ITEM_QTY, B.QTY,
         B.REGDATE, B.SEQ, B.STATUS, LEV + 1,
         PATH || B.CHILD_OBJID::TEXT,
         PATH2 || LPAD(B.SEQ::TEXT, 10, '0'),
         B.PARENT_OBJID = ANY(PATH),
         B.UNIT, B.SUPPLY_TYPE, B.MAKE_OR_BUY,
         B.RAW_MATERIAL_PART_NO, B.RAW_MATERIAL_SPEC, B.RAW_MATERIAL, B.RAW_MATERIAL_SIZE,
         B.PROCESSING_VENDOR, B.PROCESSING_DEADLINE, B.GRINDING_DEADLINE,
         B.REQUIRED_QTY, B.ORDER_QTY, B.PRODUCTION_QTY, B.STOCK_QTY, B.SHORTAGE_QTY,
         B.VENDOR, B.UNIT_PRICE, B.PROCESSING_UNIT_PRICE, B.TOTAL_PRICE, B.CURRENCY,
         B.LEAD_TIME, B.MIN_ORDER_QTY, B.WRITER, B.EDITER, B.EDIT_DATE, B.REMARK
    FROM MBOM_DETAIL B
    JOIN VIEW_BOM ON B.PARENT_OBJID = VIEW_BOM.CHILD_OBJID
                 AND VIEW_BOM.MBOM_HEADER_OBJID = B.MBOM_HEADER_OBJID
                 AND B.STATUS = 'ACTIVE'
)
SELECT
  V.MBOM_HEADER_OBJID AS bom_report_objid,
  V.OBJID             AS objid,
  V.PARENT_OBJID      AS parent_objid,
  V.CHILD_OBJID       AS child_objid,
  V.PART_OBJID        AS part_objid,
  V.PART_NO           AS part_no,
  V.PART_NAME         AS part_name,
  V.QTY               AS qty,
  V.ITEM_QTY          AS item_qty,
  V.QTY_TEMP          AS qty_temp,
  V.LEV               AS level,
  (SELECT COUNT(*) FROM MBOM_DETAIL WHERE PARENT_OBJID = V.CHILD_OBJID) AS sub_part_cnt,
  V.SEQ               AS seq,
  V.STATUS            AS status,
  V.UNIT              AS unit,
  V.SUPPLY_TYPE       AS supply_type,
  V.MAKE_OR_BUY       AS make_or_buy,
  V.RAW_MATERIAL_PART_NO AS raw_material_no,
  V.RAW_MATERIAL_SPEC AS raw_material_spec,
  V.RAW_MATERIAL      AS raw_material,
  V.RAW_MATERIAL_SIZE AS size,
  V.PROCESSING_VENDOR AS processing_vendor,
  (SELECT CLIENT_NM FROM CLIENT_MNG WHERE OBJID::VARCHAR = V.PROCESSING_VENDOR) AS processing_vendor_name,
  V.PROCESSING_DEADLINE AS processing_deadline,
  V.GRINDING_DEADLINE  AS grinding_deadline,
  V.REQUIRED_QTY      AS required_qty,
  V.ORDER_QTY         AS order_qty,
  V.PRODUCTION_QTY    AS production_qty,
  V.STOCK_QTY         AS stock_qty,
  V.SHORTAGE_QTY      AS shortage_qty,
  V.VENDOR            AS vendor,
  (SELECT CLIENT_NM FROM CLIENT_MNG WHERE OBJID::VARCHAR = V.VENDOR) AS vendor_name,
  V.UNIT_PRICE        AS unit_price,
  V.PROCESSING_UNIT_PRICE AS processing_unit_price,
  V.TOTAL_PRICE       AS total_price,
  V.CURRENCY          AS currency,
  V.LEAD_TIME         AS lead_time,
  V.MIN_ORDER_QTY     AS min_order_qty,
  V.WRITER            AS writer,
  TO_CHAR(V.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS regdate,
  V.EDITER            AS editer,
  CASE WHEN V.EDIT_DATE IS NOT NULL THEN TO_CHAR(V.EDIT_DATE, 'YYYY-MM-DD HH24:MI:SS') END AS edit_date,
  V.REMARK            AS remark,
  CASE WHEN V.LEV = 1 THEN V.OBJID END AS root_objid,
  CASE WHEN V.LEV = 1 THEN V.OBJID END AS sub_root_objid,
  1                   AS leaf,
  P.SPEC, P.MATERIAL, P.WEIGHT, P.PART_TYPE, P.REVISION, P.MAKER,
  P.THICKNESS, P.WIDTH, P.HEIGHT, P.OUT_DIAMETER, P.IN_DIAMETER, P.LENGTH,
  P.SOURCING_CODE, P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT,
  (SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.UNIT) AS unit_title,
  (SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.PART_TYPE) AS part_type_title,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD')) AS cu01_cnt,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD')) AS cu03_cnt,
  COALESCE((SELECT NULLIF(MP.UNIT_QTY, '')::numeric    FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_qty,
  COALESCE((SELECT NULLIF(MP.UNIT_LENGTH, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_length
FROM VIEW_BOM V
INNER JOIN PART_MNG P ON P.OBJID = V.PART_OBJID
ORDER BY V.PATH2
`;

const STRUCTURE_ONLY_SQL = `
WITH RECURSIVE VIEW_BOM(
  MBOM_HEADER_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
  PART_OBJID, PART_NO, PART_NAME, QTY, ITEM_QTY, QTY_TEMP,
  REGDATE, SEQ, STATUS, LEV, PATH, PATH2, CYCLE,
  UNIT, WRITER, RAW_MATERIAL_PART_NO
) AS (
  SELECT A.MBOM_HEADER_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
         A.PART_OBJID, A.PART_NO, A.PART_NAME, A.QTY, A.QTY, A.QTY,
         A.REGDATE, A.SEQ, A.STATUS, 1,
         ARRAY [A.CHILD_OBJID::TEXT],
         ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
         FALSE,
         A.UNIT, A.WRITER, A.RAW_MATERIAL_PART_NO
    FROM MBOM_DETAIL A
   WHERE 1=1
     AND (A.PARENT_OBJID IS NULL OR A.PARENT_OBJID = '')
     AND A.MBOM_HEADER_OBJID = $1
     AND A.STATUS = 'ACTIVE'
  UNION ALL
  SELECT B.MBOM_HEADER_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
         B.PART_OBJID, B.PART_NO, B.PART_NAME, B.QTY, B.QTY, B.QTY,
         B.REGDATE, B.SEQ, B.STATUS, LEV + 1,
         PATH || B.CHILD_OBJID::TEXT,
         PATH2 || LPAD(B.SEQ::TEXT, 10, '0'),
         B.PARENT_OBJID = ANY(PATH),
         B.UNIT, B.WRITER, B.RAW_MATERIAL_PART_NO
    FROM MBOM_DETAIL B
    JOIN VIEW_BOM ON B.PARENT_OBJID = VIEW_BOM.CHILD_OBJID
                 AND VIEW_BOM.MBOM_HEADER_OBJID = B.MBOM_HEADER_OBJID
                 AND B.STATUS = 'ACTIVE'
)
SELECT
  V.MBOM_HEADER_OBJID AS bom_report_objid,
  V.OBJID             AS objid,
  V.PARENT_OBJID      AS parent_objid,
  V.CHILD_OBJID       AS child_objid,
  V.PART_OBJID        AS part_objid,
  V.PART_NO           AS part_no,
  V.PART_NAME         AS part_name,
  V.QTY               AS qty,
  V.ITEM_QTY          AS item_qty,
  V.QTY_TEMP          AS qty_temp,
  V.LEV               AS level,
  (SELECT COUNT(*) FROM MBOM_DETAIL WHERE PARENT_OBJID = V.CHILD_OBJID) AS sub_part_cnt,
  V.SEQ, V.STATUS, V.UNIT, V.WRITER,
  TO_CHAR(V.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS regdate,
  NULL::text AS supply_type, NULL::text AS make_or_buy,
  NULL::text AS raw_material_no, NULL::text AS raw_material_spec,
  NULL::text AS raw_material, NULL::text AS size,
  NULL::text AS processing_vendor, NULL::text AS processing_deadline, NULL::text AS grinding_deadline,
  NULL::numeric AS required_qty, NULL::numeric AS order_qty, NULL::numeric AS production_qty,
  NULL::numeric AS stock_qty, NULL::numeric AS shortage_qty,
  NULL::text AS vendor, NULL::numeric AS unit_price, NULL::numeric AS processing_unit_price,
  NULL::numeric AS total_price, NULL::text AS currency,
  NULL::int AS lead_time, NULL::numeric AS min_order_qty,
  NULL::text AS editer, NULL::text AS edit_date, NULL::text AS remark,
  CASE WHEN V.LEV = 1 THEN V.OBJID END AS root_objid,
  CASE WHEN V.LEV = 1 THEN V.OBJID END AS sub_root_objid,
  1 AS leaf,
  P.SPEC, P.MATERIAL, P.WEIGHT, P.PART_TYPE, P.REVISION, P.MAKER,
  P.THICKNESS, P.WIDTH, P.HEIGHT, P.OUT_DIAMETER, P.IN_DIAMETER, P.LENGTH,
  P.SOURCING_CODE, P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT,
  (SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.UNIT) AS unit_title,
  (SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.PART_TYPE) AS part_type_title,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD'))         AS cu01_cnt,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD'))     AS cu03_cnt,
  COALESCE((SELECT NULLIF(MP.UNIT_QTY, '')::numeric    FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_qty,
  COALESCE((SELECT NULLIF(MP.UNIT_LENGTH, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_length
FROM VIEW_BOM V
INNER JOIN PART_MNG P ON P.OBJID = V.PART_OBJID
ORDER BY V.PATH2
`;

// TEMPLATE 트리는 SAVED 와 동일한 CTE — 운영판도 mbom_detail 한 테이블에서 가져옴.
// 차이: TEMPLATE 은 ORDER_QTY/PRODUCTION_QTY 를 결과에서 표시하지 않음 (재계산 대상).
const TEMPLATE_TREE_SQL = `
WITH RECURSIVE VIEW_BOM(
  MBOM_HEADER_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
  PART_OBJID, PART_NO, PART_NAME, QTY, ITEM_QTY, QTY_TEMP,
  REGDATE, SEQ, STATUS, LEV, PATH, PATH2, CYCLE,
  UNIT, SUPPLY_TYPE, MAKE_OR_BUY,
  RAW_MATERIAL_PART_NO, RAW_MATERIAL_SPEC, RAW_MATERIAL, RAW_MATERIAL_SIZE,
  PROCESSING_VENDOR, PROCESSING_DEADLINE, GRINDING_DEADLINE,
  REQUIRED_QTY, WRITER, EDITER, EDIT_DATE, REMARK
) AS (
  SELECT A.MBOM_HEADER_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
         A.PART_OBJID, A.PART_NO, A.PART_NAME, A.QTY, A.QTY, A.QTY,
         A.REGDATE, A.SEQ, A.STATUS, 1,
         ARRAY [A.CHILD_OBJID::TEXT],
         ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
         FALSE,
         A.UNIT, A.SUPPLY_TYPE, A.MAKE_OR_BUY,
         A.RAW_MATERIAL_PART_NO, A.RAW_MATERIAL_SPEC, A.RAW_MATERIAL, A.RAW_MATERIAL_SIZE,
         A.PROCESSING_VENDOR, A.PROCESSING_DEADLINE, A.GRINDING_DEADLINE,
         A.REQUIRED_QTY, A.WRITER, A.EDITER, A.EDIT_DATE, A.REMARK
    FROM MBOM_DETAIL A
   WHERE 1=1
     AND (A.PARENT_OBJID IS NULL OR A.PARENT_OBJID = '')
     AND A.MBOM_HEADER_OBJID = $1
     AND A.STATUS = 'ACTIVE'
  UNION ALL
  SELECT B.MBOM_HEADER_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
         B.PART_OBJID, B.PART_NO, B.PART_NAME, B.QTY, B.QTY, B.QTY,
         B.REGDATE, B.SEQ, B.STATUS, LEV + 1,
         PATH || B.CHILD_OBJID::TEXT,
         PATH2 || LPAD(B.SEQ::TEXT, 10, '0'),
         B.PARENT_OBJID = ANY(PATH),
         B.UNIT, B.SUPPLY_TYPE, B.MAKE_OR_BUY,
         B.RAW_MATERIAL_PART_NO, B.RAW_MATERIAL_SPEC, B.RAW_MATERIAL, B.RAW_MATERIAL_SIZE,
         B.PROCESSING_VENDOR, B.PROCESSING_DEADLINE, B.GRINDING_DEADLINE,
         B.REQUIRED_QTY, B.WRITER, B.EDITER, B.EDIT_DATE, B.REMARK
    FROM MBOM_DETAIL B
    JOIN VIEW_BOM ON B.PARENT_OBJID = VIEW_BOM.CHILD_OBJID
                 AND VIEW_BOM.MBOM_HEADER_OBJID = B.MBOM_HEADER_OBJID
                 AND B.STATUS = 'ACTIVE'
)
SELECT
  V.MBOM_HEADER_OBJID AS bom_report_objid,
  V.OBJID, V.PARENT_OBJID AS parent_objid, V.CHILD_OBJID AS child_objid,
  V.PART_OBJID AS part_objid, V.PART_NO AS part_no, V.PART_NAME AS part_name,
  V.QTY AS qty, V.ITEM_QTY AS item_qty, V.QTY_TEMP AS qty_temp,
  V.LEV AS level,
  (SELECT COUNT(*) FROM MBOM_DETAIL WHERE PARENT_OBJID = V.CHILD_OBJID) AS sub_part_cnt,
  V.SEQ AS seq, V.STATUS AS status,
  V.UNIT, V.SUPPLY_TYPE AS supply_type, V.MAKE_OR_BUY AS make_or_buy,
  V.RAW_MATERIAL_PART_NO AS raw_material_no,
  V.RAW_MATERIAL_SPEC AS raw_material_spec,
  V.RAW_MATERIAL AS raw_material,
  V.RAW_MATERIAL_SIZE AS size,
  V.PROCESSING_VENDOR AS processing_vendor,
  V.PROCESSING_DEADLINE AS processing_deadline,
  V.GRINDING_DEADLINE  AS grinding_deadline,
  V.REQUIRED_QTY AS required_qty,
  NULL::numeric AS order_qty, NULL::numeric AS production_qty,
  V.WRITER AS writer,
  TO_CHAR(V.REGDATE, 'YYYY-MM-DD HH24:MI:SS') AS regdate,
  V.EDITER AS editer,
  CASE WHEN V.EDIT_DATE IS NOT NULL THEN TO_CHAR(V.EDIT_DATE, 'YYYY-MM-DD HH24:MI:SS') END AS edit_date,
  V.REMARK AS remark,
  CASE WHEN V.LEV = 1 THEN V.OBJID END AS root_objid,
  CASE WHEN V.LEV = 1 THEN V.OBJID END AS sub_root_objid,
  1 AS leaf,
  P.SPEC, P.MATERIAL, P.WEIGHT, P.PART_TYPE, P.REVISION, P.MAKER,
  P.THICKNESS, P.WIDTH, P.HEIGHT, P.OUT_DIAMETER, P.IN_DIAMETER, P.LENGTH,
  P.SOURCING_CODE, P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT,
  (SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.UNIT) AS unit_title,
  (SELECT CODE_NAME FROM COMM_CODE CC WHERE CC.CODE_ID = P.PART_TYPE) AS part_type_title,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD'))         AS cu01_cnt,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD'))     AS cu03_cnt,
  COALESCE((SELECT NULLIF(MP.UNIT_QTY, '')::numeric    FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_qty,
  COALESCE((SELECT NULLIF(MP.UNIT_LENGTH, '')::numeric FROM PART_MNG MP WHERE MP.PART_NO = V.RAW_MATERIAL_PART_NO LIMIT 1), 0) AS part_unit_length
FROM VIEW_BOM V
LEFT JOIN PART_MNG P ON V.PART_OBJID = P.OBJID
ORDER BY V.PATH2
`;

// ─── 저장 (PR-B1) ───────────────────────────────────────────
//
// 운영판 ProductionPlanningController.saveMbom (1549~1645) + 서비스 saveMbom (1192~1574) 1:1.
// 매퍼: insertMbomHeader / updateMbomHeader / insertMbomDetail / updateMbomDetail
//      / deleteMbomDetailByObjid / insertMbomHistory / updateProjectMbomStatus.
//
// 분기 처리:
//   isUpdate=false (최초 저장)  — 새 mbom_header 생성 + child_objid 재매핑 후 detail 일괄 insert
//                                  + history(CREATE) + project_mgmt.mbom_status='Y'
//   isUpdate=true  (수정 저장)  — 기존 mbom_header.objid 조회 → updateMbomHeader
//                                  → mbom_data 의 objid 기준 UPSERT(insert/update) + 누락분 delete
//                                  + history(UPDATE, 변경 행수 description)

export interface MbomSaveRow {
  objid?: string | null;
  parent_objid?: string | null;
  child_objid?: string | null;
  seq?: number | string | null;
  level?: number | string | null;
  part_objid?: string | number | null;
  part_no?: string | null;
  part_name?: string | null;
  qty?: number | string | null;
  item_qty?: number | string | null;
  unit?: string | null;
  supply_type?: string | null;
  make_or_buy?: string | null;
  raw_material_no?: string | null;
  raw_material_spec?: string | null;
  raw_material?: string | null;
  size?: string | null;          // tree row alias → raw_material_size
  raw_material_size?: string | null;
  processing_vendor?: string | null;
  processing_deadline?: string | null;
  grinding_deadline?: string | null;
  required_qty?: number | string | null;
  order_qty?: number | string | null;
  production_qty?: number | string | null;
  stock_qty?: number | string | null;
  shortage_qty?: number | string | null;
  vendor?: string | null;
  unit_price?: number | string | null;
  processing_unit_price?: number | string | null;
  total_price?: number | string | null;
  currency?: string | null;
  lead_time?: number | string | null;
  min_order_qty?: number | string | null;
  remark?: string | null;
}

export interface MbomSavePayload {
  project_obj_id: string;
  is_update: boolean;
  mbom_part_no?: string | null;   // 최상위 제품 변경 시 (PR-B1 에서는 신규/유지만)
  rows: MbomSaveRow[];
}

export interface MbomSaveResult {
  mode: "CREATE" | "UPDATE";
  mbom_header_objid: string;
  mbom_no: string;
  inserted: number;
  updated: number;
  deleted: number;
}

// generateMbomNo — wace generateMbomNo(EBOM/TEMPLATE) 1:1.
// 패턴: M-{cleanPartNo}-{YYMMDD}-{NN}  (NN = 동일 prefix 마지막+1, 미존재 시 01)
async function generateMbomNo(
  client: any,
  sourceBomType: string,
  basePartNo: string,
): Promise<string> {
  let cleanPartNo = (basePartNo || "").trim();
  if (cleanPartNo.startsWith("M-")) cleanPartNo = cleanPartNo.substring(2);
  const now = new Date();
  const yy = String(now.getFullYear()).slice(-2);
  const mm = String(now.getMonth() + 1).padStart(2, "0");
  const dd = String(now.getDate()).padStart(2, "0");
  const dateStr = `${yy}${mm}${dd}`;
  const prefix = `M-${cleanPartNo}-${dateStr}`;

  const r = await client.query(
    `SELECT MBOM_NO FROM MBOM_HEADER
      WHERE MBOM_NO LIKE $1 || '-%'
      ORDER BY MBOM_NO DESC LIMIT 1`,
    [prefix],
  );
  let seq = 1;
  if (r.rows[0]?.mbom_no) {
    const m = String(r.rows[0].mbom_no).match(/-(\d{2})$/);
    if (m) seq = Math.min(99, Number(m[1]) + 1);
  }
  return `${prefix}-${String(seq).padStart(2, "0")}`;
}

const DETAIL_INSERT_SQL = `
INSERT INTO MBOM_DETAIL (
  OBJID, MBOM_HEADER_OBJID, PARENT_OBJID, CHILD_OBJID, SEQ, LEVEL,
  PART_OBJID, PART_NO, PART_NAME, QTY, ITEM_QTY, UNIT,
  SUPPLY_TYPE, MAKE_OR_BUY,
  RAW_MATERIAL_PART_NO, RAW_MATERIAL_SPEC, RAW_MATERIAL, RAW_MATERIAL_SIZE,
  PROCESSING_VENDOR, PROCESSING_DEADLINE, GRINDING_DEADLINE,
  REQUIRED_QTY, ORDER_QTY, PRODUCTION_QTY, STOCK_QTY, SHORTAGE_QTY,
  VENDOR, UNIT_PRICE, PROCESSING_UNIT_PRICE, TOTAL_PRICE, CURRENCY,
  LEAD_TIME, MIN_ORDER_QTY,
  STATUS, WRITER, REGDATE, EDITER, EDIT_DATE, REMARK
) VALUES (
  $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,
  $15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,
  $27,$28,$29,$30,$31,$32,$33,
  'ACTIVE', $34, NOW(), $34, NOW(), $35
)`;

const DETAIL_UPDATE_SQL = `
UPDATE MBOM_DETAIL SET
  PARENT_OBJID = NULLIF($1, ''),
  SEQ = $2,
  LEVEL = $3,
  PART_OBJID = $4,
  PART_NO = NULLIF($5, ''),
  PART_NAME = NULLIF($6, ''),
  QTY = $7,
  ITEM_QTY = $8,
  UNIT = NULLIF($9, ''),
  SUPPLY_TYPE = NULLIF($10, ''),
  MAKE_OR_BUY = NULLIF($11, ''),
  RAW_MATERIAL_PART_NO = NULLIF($12, ''),
  RAW_MATERIAL_SPEC = NULLIF($13, ''),
  RAW_MATERIAL = NULLIF($14, ''),
  RAW_MATERIAL_SIZE = NULLIF($15, ''),
  PROCESSING_VENDOR = NULLIF($16, ''),
  PROCESSING_DEADLINE = NULLIF($17, ''),
  GRINDING_DEADLINE = NULLIF($18, ''),
  REQUIRED_QTY = $19,
  ORDER_QTY = $20,
  PRODUCTION_QTY = $21,
  STOCK_QTY = $22,
  SHORTAGE_QTY = $23,
  EDITER = $24,
  EDIT_DATE = NOW(),
  REMARK = NULLIF($25, '')
WHERE OBJID = $26`;

function n(v: any): number | null {
  if (v == null || v === "") return null;
  const num = Number(v);
  return Number.isFinite(num) ? num : null;
}
function s(v: any): string | null {
  if (v == null) return null;
  const str = String(v).trim();
  return str === "" ? null : str;
}
function bi(v: any): string | null {
  // part_objid 는 bigint — 숫자/문자열 모두 수용, 빈값/NaN 은 null
  if (v == null || v === "") return null;
  const num = Number(v);
  return Number.isFinite(num) ? String(Math.trunc(num)) : null;
}

function detailInsertParams(
  row: MbomSaveRow,
  objid: string,
  mbomHeaderObjid: string,
  childObjid: string,
  parentObjid: string | null,
  userId: string,
): any[] {
  return [
    objid,
    mbomHeaderObjid,
    parentObjid,
    childObjid,
    n(row.seq) ?? 999,
    n(row.level) ?? 1,
    bi(row.part_objid),
    s(row.part_no),
    s(row.part_name),
    n(row.qty),
    n(row.item_qty),
    s(row.unit),
    s(row.supply_type),
    s(row.make_or_buy),
    s(row.raw_material_no),
    s(row.raw_material_spec),
    s(row.raw_material),
    s(row.raw_material_size ?? row.size),
    s(row.processing_vendor),
    s(row.processing_deadline),
    s(row.grinding_deadline),
    n(row.required_qty),
    n(row.order_qty),
    n(row.production_qty),
    n(row.stock_qty),
    n(row.shortage_qty),
    s(row.vendor),
    n(row.unit_price),
    n(row.processing_unit_price),
    n(row.total_price),
    s(row.currency) ?? "KRW",
    n(row.lead_time),
    n(row.min_order_qty),
    userId,
    s(row.remark),
  ];
}

function detailUpdateParams(row: MbomSaveRow, objid: string, userId: string): any[] {
  return [
    s(row.parent_objid) ?? "",
    n(row.seq) ?? 999,
    n(row.level) ?? 1,
    bi(row.part_objid),
    s(row.part_no) ?? "",
    s(row.part_name) ?? "",
    n(row.qty),
    n(row.item_qty),
    s(row.unit) ?? "",
    s(row.supply_type) ?? "",
    s(row.make_or_buy) ?? "",
    s(row.raw_material_no) ?? "",
    s(row.raw_material_spec) ?? "",
    s(row.raw_material) ?? "",
    s(row.raw_material_size ?? row.size) ?? "",
    s(row.processing_vendor) ?? "",
    s(row.processing_deadline) ?? "",
    s(row.grinding_deadline) ?? "",
    n(row.required_qty),
    n(row.order_qty),
    n(row.production_qty),
    n(row.stock_qty),
    n(row.shortage_qty),
    userId,
    s(row.remark) ?? "",
    objid,
  ];
}

export async function save(payload: MbomSavePayload, sessionUserId: string): Promise<MbomSaveResult> {
  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    const projectObjId = String(payload.project_obj_id ?? "").trim();
    if (!projectObjId) throw new Error("project_obj_id 누락");
    const userId = String(sessionUserId ?? "").trim() || "system";

    // 1) 프로젝트 + 할당 정보 조회 (sourceBomType + basePartNo)
    const proj = await client.query(
      `SELECT PM.OBJID::VARCHAR AS objid,
              PM.PART_NO AS part_no, PM.PART_NAME AS part_name,
              PM.SOURCE_BOM_TYPE AS source_bom_type,
              PM.SOURCE_EBOM_OBJID AS source_ebom_objid,
              PM.SOURCE_MBOM_OBJID AS source_mbom_objid,
              CM.PRODUCT AS product_code,
              (SELECT PBR.PART_NO FROM PART_BOM_REPORT PBR
                WHERE PBR.OBJID::VARCHAR = PM.SOURCE_EBOM_OBJID LIMIT 1) AS ebom_part_no,
              (SELECT MH.MBOM_NO FROM MBOM_HEADER MH
                WHERE MH.OBJID = PM.SOURCE_MBOM_OBJID LIMIT 1) AS source_mbom_no
         FROM PROJECT_MGMT PM
         INNER JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
        WHERE PM.OBJID::VARCHAR = $1 LIMIT 1`,
      [projectObjId],
    );
    if (!proj.rows[0]) throw new Error("프로젝트 정보를 찾을 수 없습니다");
    const p = proj.rows[0];

    let sourceBomType: string = p.source_bom_type ?? "";
    let sourceEbomObjid: string | null = null;
    let sourceMbomObjid: string | null = null;
    let basePartNo = "";

    if (sourceBomType === "EBOM") {
      sourceEbomObjid = p.source_ebom_objid ?? null;
      basePartNo = p.ebom_part_no ?? p.part_no ?? "";
    } else if (sourceBomType === "MBOM") {
      sourceMbomObjid = p.source_mbom_objid ?? null;
      basePartNo = p.source_mbom_no ?? p.part_no ?? "";
    } else {
      // Machine 이외(product != 0000928) + part_no 가 있으면 TEMPLATE
      if (p.product_code !== "0000928" && p.part_no) {
        sourceBomType = "TEMPLATE";
        basePartNo = p.part_no;
      } else {
        throw new Error("M-BOM 기준 정보가 없습니다. BOM 복사 팝업에서 먼저 기준을 설정해주세요.");
      }
    }

    let mbomHeaderObjid: string;
    let mbomNo: string;
    let inserted = 0, updated = 0, deleted = 0;
    let mode: "CREATE" | "UPDATE";

    if (payload.is_update) {
      // ── UPDATE 분기 ──
      const exist = await client.query(
        `SELECT OBJID AS objid, MBOM_NO AS mbom_no FROM MBOM_HEADER
          WHERE PROJECT_OBJID = $1 AND STATUS = 'Y'
          ORDER BY REGDATE DESC LIMIT 1`,
        [projectObjId],
      );
      if (!exist.rows[0]) throw new Error("수정 대상 M-BOM 헤더를 찾을 수 없습니다");
      mbomHeaderObjid = exist.rows[0].objid;
      mbomNo = exist.rows[0].mbom_no;
      mode = "UPDATE";

      // 헤더 update
      await client.query(
        `UPDATE MBOM_HEADER SET
           PART_NO = $1, PART_NAME = $2,
           EDITER = $3, EDIT_DATE = NOW()
         WHERE OBJID = $4`,
        [p.part_no ?? "", p.part_name ?? "", userId, mbomHeaderObjid],
      );

      // 기존 detail objid 수집
      const existRes = await client.query(
        `SELECT OBJID AS objid FROM MBOM_DETAIL WHERE MBOM_HEADER_OBJID = $1`,
        [mbomHeaderObjid],
      );
      const existIds = new Set<string>(existRes.rows.map((r: any) => r.objid));
      const incomingIds = new Set<string>();

      // 신규 행의 client temp- child_objid → 서버 발급 createObjId 매핑
      // (객체 ID 안정성 + DB 에 temp- 잔존 방지)
      const tempChildMap = new Map<string, string>();
      for (const row of payload.rows ?? []) {
        if (!s(row.objid)) {
          const newId = createObjId();
          const tempChild = s(row.child_objid);
          if (tempChild) tempChildMap.set(tempChild, newId);
          // 임시로 row 자체에 새 objid 부여 — 이어지는 루프에서 다시 읽지 않도록
          (row as any).__newObjid = newId;
        }
      }

      // UPSERT
      for (const row of payload.rows ?? []) {
        let objid = s(row.objid) ?? "";
        const isNew = !objid;
        if (isNew) objid = (row as any).__newObjid as string;
        // 새 행이면 child_objid 도 새 ID 로 통일, 기존 행이면 그대로
        let childObjid = isNew ? objid : (s(row.child_objid) ?? objid);
        // parent_objid: temp- 참조면 신규 발급 ID 로 remap, 아니면 그대로
        const rawParent = s(row.parent_objid);
        const parentObjid = rawParent && tempChildMap.has(rawParent)
          ? tempChildMap.get(rawParent)!
          : rawParent;
        incomingIds.add(objid);

        if (existIds.has(objid)) {
          await client.query(DETAIL_UPDATE_SQL, detailUpdateParams({ ...row, parent_objid: parentObjid }, objid, userId));
          updated++;
        } else {
          await client.query(
            DETAIL_INSERT_SQL,
            detailInsertParams(row, objid, mbomHeaderObjid, childObjid, parentObjid, userId),
          );
          inserted++;
        }
      }

      // 누락 행 delete
      for (const oldId of existIds) {
        if (!incomingIds.has(oldId)) {
          await client.query(`DELETE FROM MBOM_DETAIL WHERE OBJID = $1`, [oldId]);
          deleted++;
        }
      }

      // history UPDATE
      await insertHistory(
        client,
        mbomHeaderObjid,
        "UPDATE",
        `${inserted + updated + deleted}개 항목 처리 (insert=${inserted}, update=${updated}, delete=${deleted})`,
        userId,
      );
    } else {
      // ── CREATE 분기 ──
      mbomHeaderObjid = createObjId();
      mbomNo = await generateMbomNo(client, sourceBomType, basePartNo);
      mode = "CREATE";

      await client.query(
        `INSERT INTO MBOM_HEADER (
           OBJID, MBOM_NO, SOURCE_BOM_TYPE, SOURCE_EBOM_OBJID, SOURCE_MBOM_OBJID,
           PROJECT_OBJID, PART_NO, PART_NAME, STATUS, MBOM_STATUS,
           WRITER, REGDATE
         ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'Y','DRAFT',$9,NOW())`,
        [
          mbomHeaderObjid, mbomNo, sourceBomType,
          sourceEbomObjid, sourceMbomObjid,
          projectObjId, p.part_no ?? "", p.part_name ?? "",
          userId,
        ],
      );

      // child_objid 재매핑 (E-BOM/MBOM 의 기존 child_objid → 새 child_objid)
      const childMap = new Map<string, string>();
      for (const row of payload.rows ?? []) {
        const oldChild = s(row.child_objid);
        if (oldChild && !childMap.has(oldChild)) childMap.set(oldChild, createObjId());
      }

      for (const row of payload.rows ?? []) {
        const oldChild = s(row.child_objid);
        const oldParent = s(row.parent_objid);
        const newChild = (oldChild && childMap.get(oldChild)) || createObjId();
        const newParent = oldParent ? (childMap.get(oldParent) ?? oldParent) : null;

        await client.query(
          DETAIL_INSERT_SQL,
          detailInsertParams(row, newChild, mbomHeaderObjid, newChild, newParent, userId),
        );
        inserted++;
      }

      // history CREATE
      await insertHistory(
        client,
        mbomHeaderObjid,
        "CREATE",
        `M-BOM 신규 저장 (${inserted}개 항목)`,
        userId,
      );

      // PROJECT_MGMT.MBOM_STATUS = 'Y'
      await client.query(
        `UPDATE PROJECT_MGMT SET MBOM_STATUS = 'Y', MBOM_WRITER = $1, MBOM_REGDATE = NOW()
          WHERE OBJID::VARCHAR = $2`,
        [userId, projectObjId],
      );
    }

    await client.query("COMMIT");
    return { mode, mbom_header_objid: mbomHeaderObjid, mbom_no: mbomNo, inserted, updated, deleted };
  } catch (e) {
    await client.query("ROLLBACK");
    throw e;
  } finally {
    client.release();
  }
}

// ─── BOM 할당 (PR-B5) ───────────────────────────────────────
//
// 매퍼 productionplanning.getEbomList (3221~3265) 1:1 — 할당 가능한 E-BOM 검색.
// 매퍼 productionplanning.saveBomAssignment (3545~3553) 1:1 — project_mgmt.source_bom_type/source_*_objid 업데이트.
//
// 운영판 흐름 (mBomEbomSelectPopup.do + assignEbomToMbom.do):
//   1) 사용자가 BOM 할당 다이얼로그 오픈 → E-BOM 검색 (part_bom_report)
//   2) 한 건 선택 → assign(projectObjid, 'EBOM', bomReportObjid) 호출
//   3) project_mgmt.source_bom_type='EBOM' + source_ebom_objid 저장
//   4) M-BOM 다이얼로그 재조회 → ASSIGNED_EBOM 트리 자동 표시
// (M-BOM 할당은 PR-B5 v2 — 우선 E-BOM 만)

export interface AssignableEbomFilter {
  objid?: string;              // 단건 정확매칭 (현재 할당 E-BOM 정보 조회용)
  product_cd?: string;
  search_part_no?: string;
  search_part_name?: string;
  search_from_date?: string;   // YYYY-MM-DD
  search_to_date?: string;
  limit?: number;
}

export interface AssignableEbomRow {
  objid: string;
  product_cd: string | null;
  product_name: string | null;
  part_no: string | null;
  part_name: string | null;
  status: string | null;
  revision: string | null;
  reg_date: string | null;
  writer_name: string | null;
  dept_name: string | null;
  material: string | null;
  supplier: string | null;
}

export async function searchAssignableEboms(filter: AssignableEbomFilter): Promise<AssignableEbomRow[]> {
  const pool = getPool();
  const conds: string[] = [];
  const params: any[] = [];
  let idx = 1;
  if (filter.objid) {
    conds.push(`T.OBJID::VARCHAR = $${idx++}`);
    params.push(filter.objid);
  }
  if (filter.product_cd) {
    conds.push(`T.PRODUCT_CD = $${idx++}`);
    params.push(filter.product_cd);
  }
  if (filter.search_part_no) {
    conds.push(`UPPER(T.PART_NO) LIKE '%' || UPPER($${idx++}) || '%'`);
    params.push(filter.search_part_no);
  }
  if (filter.search_part_name) {
    conds.push(`UPPER(T.PART_NAME) LIKE '%' || UPPER($${idx++}) || '%'`);
    params.push(filter.search_part_name);
  }
  if (filter.search_from_date) {
    conds.push(`T.REGDATE >= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
    params.push(filter.search_from_date);
  }
  if (filter.search_to_date) {
    conds.push(`T.REGDATE < TO_DATE($${idx++}, 'YYYY-MM-DD') + INTERVAL '1 day'`);
    params.push(filter.search_to_date);
  }
  const limit = Math.min(500, Math.max(1, Number(filter.limit) || 100));
  const sql = `
    SELECT
      T.OBJID::VARCHAR                                AS objid,
      T.PRODUCT_CD                                    AS product_cd,
      COALESCE((SELECT CODE_NAME FROM COMM_CODE
                 WHERE CODE_ID = T.PRODUCT_CD LIMIT 1), '') AS product_name,
      T.PART_NO                                       AS part_no,
      T.PART_NAME                                     AS part_name,
      T.STATUS                                        AS status,
      T.REVISION                                      AS revision,
      TO_CHAR(T.REGDATE, 'YYYY-MM-DD')                AS reg_date,
      UI.USER_NAME                                    AS writer_name,
      UI.DEPT_NAME                                    AS dept_name,
      COALESCE(PM.MATERIAL, '')                       AS material,
      COALESCE(PM.MAKER, '')                          AS supplier
    FROM PART_BOM_REPORT T
    LEFT JOIN USER_INFO UI ON UI.USER_ID = T.WRITER
    LEFT JOIN PART_MNG PM ON PM.PART_NO = T.PART_NO AND PM.STATUS = 'release'
    WHERE 1=1
      AND T.PART_NO IS NOT NULL AND TRIM(T.PART_NO) != ''
      AND T.PART_NAME IS NOT NULL AND TRIM(T.PART_NAME) != ''
      ${conds.length ? "AND " + conds.join(" AND ") : ""}
    ORDER BY T.REGDATE DESC
    LIMIT ${limit}
  `;
  const r = await pool.query(sql, params);
  return r.rows;
}

// E-BOM 미리보기 트리 — bom_report_objid 만으로 EBOM_WORKING_TREE_SQL 호출.
// 운영판 mBomEbomSelectPopup.jsp 의 iframe 미리보기 대체.
export async function previewEbomTree(bomReportObjid: string): Promise<MbomTreeResult> {
  if (!bomReportObjid) {
    return { bom_data_type: "NONE", bom_report_objid: null, max_level: 1, rows: [] };
  }
  const rows = await getEbomWorkingTree(bomReportObjid);
  return finalize("ASSIGNED_EBOM", bomReportObjid, rows);
}

export type AssignSourceType = "EBOM" | "MBOM";

export async function assignBom(
  projectObjId: string,
  sourceBomType: AssignSourceType,
  sourceBomObjId: string,
  _userId: string,
): Promise<{ success: boolean; source_bom_type: string; source_obj_id: string }> {
  const pool = getPool();
  // 매퍼 saveBomAssignment 1:1.
  // EBOM 이면 source_ebom_objid, MBOM 이면 source_mbom_objid 만 세팅 (다른 쪽 NULL).
  const sql = `
    UPDATE PROJECT_MGMT
       SET SOURCE_BOM_TYPE = $1,
           SOURCE_EBOM_OBJID = CASE WHEN $1 = 'EBOM' THEN $2 ELSE NULL END,
           SOURCE_MBOM_OBJID = CASE WHEN $1 = 'MBOM' THEN $2 ELSE NULL END
     WHERE OBJID::VARCHAR = $3
  `;
  const r = await pool.query(sql, [sourceBomType, sourceBomObjId, projectObjId]);
  if (r.rowCount === 0) throw new Error("프로젝트를 찾을 수 없습니다");
  return { success: true, source_bom_type: sourceBomType, source_obj_id: sourceBomObjId };
}

// ─── BOM 복사 다이얼로그 보조 (PR-B5+) ─────────────────────────
//
// 운영판 partMng/structureBomCopyFormPopup.jsp 진입점 보조 함수.
// 저장은 위 assignBom 재사용 (sourceBomType='EBOM'|'MBOM').

export interface AssignableMbomFilter {
  search_part_no?: string;
  search_part_name?: string;
  search_from_date?: string;
  search_to_date?: string;
  limit?: number;
}

export interface AssignableMbomRow {
  objid: string;
  mbom_no: string | null;
  part_no: string | null;
  part_name: string | null;
  reg_date: string | null;
}

// 매퍼 productionplanning.getMbomListForSelect2 (4007~4014) 1:1 + 검색 필터 확장.
export async function searchAssignableMboms(filter: AssignableMbomFilter = {}): Promise<AssignableMbomRow[]> {
  const pool = getPool();
  const conds: string[] = [];
  const params: any[] = [];
  let idx = 1;
  if (filter.search_part_no) {
    conds.push(`UPPER(MH.PART_NO) LIKE '%' || UPPER($${idx++}) || '%'`);
    params.push(filter.search_part_no);
  }
  if (filter.search_part_name) {
    conds.push(`UPPER(MH.PART_NAME) LIKE '%' || UPPER($${idx++}) || '%'`);
    params.push(filter.search_part_name);
  }
  if (filter.search_from_date) {
    conds.push(`MH.REGDATE >= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
    params.push(filter.search_from_date);
  }
  if (filter.search_to_date) {
    conds.push(`MH.REGDATE < TO_DATE($${idx++}, 'YYYY-MM-DD') + INTERVAL '1 day'`);
    params.push(filter.search_to_date);
  }
  const limit = Math.min(500, Math.max(1, Number(filter.limit) || 200));
  const sql = `
    SELECT
      MH.OBJID::VARCHAR                AS objid,
      COALESCE(MH.MBOM_NO, '')         AS mbom_no,
      COALESCE(MH.PART_NO, '')         AS part_no,
      COALESCE(MH.PART_NAME, '')       AS part_name,
      TO_CHAR(MH.REGDATE, 'YYYY-MM-DD') AS reg_date
    FROM MBOM_HEADER MH
    WHERE MH.STATUS = 'Y'
      ${conds.length ? "AND " + conds.join(" AND ") : ""}
    ORDER BY MH.REGDATE DESC, MH.MBOM_NO
    LIMIT ${limit}
  `;
  const r = await pool.query(sql, params);
  return r.rows;
}

// M-BOM 미리보기 트리 — mbom_header_objid 만으로 STRUCTURE_ONLY_SQL 호출.
// 운영판 structureBomCopyFormPopup 의 BOM 트리 미리보기 (M-BOM 선택 시).
export async function previewMbomTree(mbomHeaderObjid: string): Promise<MbomTreeResult> {
  if (!mbomHeaderObjid) {
    return { bom_data_type: "NONE", bom_report_objid: null, max_level: 1, rows: [] };
  }
  const rows = await getStructureOnly(mbomHeaderObjid);
  return finalize("ASSIGNED_MBOM", mbomHeaderObjid, rows);
}

export interface LatestMbomByPartNoRow {
  template_header_objid: string;
  mbom_no: string | null;
  project_objid: string | null;
  part_no: string | null;
  part_name: string | null;
  save_date: string | null;
}

// 매퍼 productionplanning.getLatestMbomByPartNo (3426~3445) 1:1.
// Machine(0000928) 이외 제품 + 동일 partNo + STATUS='Y' 최신 1건.
export async function getLatestMbomByPartNo(partNo: string): Promise<LatestMbomByPartNoRow | null> {
  if (!partNo || !partNo.trim()) return null;
  const pool = getPool();
  const sql = `
    SELECT
      MH.OBJID::VARCHAR                AS template_header_objid,
      MH.MBOM_NO                       AS mbom_no,
      MH.PROJECT_OBJID::VARCHAR        AS project_objid,
      MH.PART_NO                       AS part_no,
      MH.PART_NAME                     AS part_name,
      TO_CHAR(MH.REGDATE, 'YYYY-MM-DD') AS save_date
    FROM MBOM_HEADER MH
      INNER JOIN PROJECT_MGMT PM ON MH.PROJECT_OBJID = PM.OBJID
      INNER JOIN CONTRACT_MGMT CM ON PM.CONTRACT_OBJID = CM.OBJID
    WHERE MH.PART_NO = $1
      AND MH.STATUS = 'Y'
      AND CM.PRODUCT != '0000928'
    ORDER BY MH.REGDATE DESC
    LIMIT 1
  `;
  const r = await pool.query(sql, [partNo.trim()]);
  return r.rows[0] ?? null;
}

// ─── 변경이력 조회 (PR-B4) ──────────────────────────────────
//
// 매퍼 productionplanning.getMbomHistory (3448~3470) 1:1.
// project_objid 로 그 프로젝트의 모든 mbom_header 변경이력 시간순 (최신 우선).

export interface MbomHistoryRow {
  objid: string;
  mbom_header_objid: string;
  change_type: string;
  change_description: string | null;
  change_user: string | null;
  change_user_name: string | null;
  change_date: string;
  mbom_part_no: string | null;
  mbom_regdate: string | null;
}

export async function getHistory(projectObjid: string): Promise<MbomHistoryRow[]> {
  const pool = getPool();
  const sql = `
    SELECT
      MH.OBJID                       AS objid,
      MH.MBOM_HEADER_OBJID           AS mbom_header_objid,
      MH.CHANGE_TYPE                 AS change_type,
      MH.CHANGE_DESCRIPTION          AS change_description,
      MH.CHANGE_USER                 AS change_user,
      COALESCE(
        (SELECT USER_NAME FROM USER_INFO WHERE USER_ID = MH.CHANGE_USER LIMIT 1),
        MH.CHANGE_USER
      )                              AS change_user_name,
      TO_CHAR(MH.CHANGE_DATE, 'YYYY-MM-DD HH24:MI:SS') AS change_date,
      MHD.MBOM_NO                    AS mbom_part_no,
      TO_CHAR(MHD.REGDATE, 'YYYY-MM-DD HH24:MI:SS')    AS mbom_regdate
    FROM MBOM_HISTORY MH
    INNER JOIN MBOM_HEADER MHD ON MH.MBOM_HEADER_OBJID = MHD.OBJID
    WHERE MHD.PROJECT_OBJID = $1
    ORDER BY MH.CHANGE_DATE DESC
  `;
  const r = await pool.query(sql, [projectObjid]);
  return r.rows;
}

// ─── 구매리스트 생성 (PR-B3) ────────────────────────────────
//
// wace 매퍼 salesMng.xml `getNextRequestMngNo` + `insertSalesRequestMasterFromMBom` 1:1
// (서비스 SalesMngService.createPurchaseListFromMBom, 매퍼 라인 3960~3997).
//
// 검증: M-BOM 헤더 존재 + 동일 mbom_header_objid 로 이미 생성된 sales_request_master 없음.
// 입력: mbomHeaderObjid (mbom_header.objid), projectMgmtObjid (project_mgmt.objid).
// 처리: 단일 INSERT, BEGIN/COMMIT 트랜잭션. mbom_header 역링크 컬럼 없음 (운영 동일).

export interface CreateSalesRequestResult {
  objid: string;
  request_mng_no: string;
  mbom_header_objid: string;
}

export async function createSalesRequest(
  mbomHeaderObjid: string,
  projectMgmtObjid: string,
  sessionUserId: string,
): Promise<CreateSalesRequestResult> {
  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    const mbom = String(mbomHeaderObjid ?? "").trim();
    const proj = String(projectMgmtObjid ?? "").trim();
    if (!mbom) throw new Error("MBOM_HEADER_OBJID 누락");
    if (!proj) throw new Error("PROJECT_MGMT_OBJID 누락");
    const userId = String(sessionUserId ?? "").trim() || "system";

    // 1) M-BOM 헤더 존재 확인
    const mh = await client.query(
      `SELECT OBJID FROM MBOM_HEADER WHERE OBJID::VARCHAR = $1 AND STATUS = 'Y' LIMIT 1`,
      [mbom],
    );
    if (!mh.rows[0]) throw new Error("저장된 M-BOM 헤더를 찾을 수 없습니다");

    // 2) 동일 MBOM_HEADER 로 이미 생성된 구매리스트 차단 (운영판 sweetalert 가드 1:1)
    const dup = await client.query(
      `SELECT OBJID, REQUEST_MNG_NO FROM SALES_REQUEST_MASTER
        WHERE MBOM_HEADER_OBJID = $1 LIMIT 1`,
      [mbom],
    );
    if (dup.rows[0]) {
      throw new Error(`이미 생성된 구매리스트가 있습니다 (${dup.rows[0].request_mng_no})`);
    }

    // 3) 채번: R + YYYYMMDD + - + 3자리 (운영 getNextRequestMngNo 1:1)
    const seqRes = await client.query(
      `SELECT 'R' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' ||
              LPAD(
                (COALESCE(MAX(SUBSTR(REQUEST_MNG_NO, 11, 13)), '0')::INTEGER + 1)::TEXT,
                3, '0'
              ) AS request_mng_no
         FROM SALES_REQUEST_MASTER
        WHERE DOC_TYPE IN ('PURCHASE_REQUEST', 'PURCHASE_REG') OR DOC_TYPE IS NULL`,
    );
    const requestMngNo: string = seqRes.rows[0]?.request_mng_no;
    if (!requestMngNo) throw new Error("REQUEST_MNG_NO 채번 실패");

    // 4) INSERT (운영 insertSalesRequestMasterFromMBom 1:1, PROJECT_NO 컬럼에 PROJECT_MGMT.OBJID 저장)
    const newObjid = createObjId();
    await client.query(
      `INSERT INTO SALES_REQUEST_MASTER (
         OBJID, REQUEST_MNG_NO, PROJECT_NO, MBOM_HEADER_OBJID,
         REQUEST_USER_ID, STATUS, WRITER, REGDATE, DOC_TYPE
       ) VALUES ($1, $2, $3, $4, $5, 'create', $6, NOW(), 'PURCHASE_REQUEST')`,
      [newObjid, requestMngNo, proj, mbom, userId, userId],
    );

    await client.query("COMMIT");
    return {
      objid: newObjid,
      request_mng_no: requestMngNo,
      mbom_header_objid: mbom,
    };
  } catch (e) {
    await client.query("ROLLBACK");
    throw e;
  } finally {
    client.release();
  }
}

async function insertHistory(
  client: any,
  mbomHeaderObjid: string,
  changeType: "CREATE" | "UPDATE",
  description: string,
  userId: string,
) {
  await client.query(
    `INSERT INTO MBOM_HISTORY (
       OBJID, MBOM_HEADER_OBJID, CHANGE_TYPE, CHANGE_DESCRIPTION,
       BEFORE_DATA, AFTER_DATA, CHANGE_USER, CHANGE_DATE
     ) VALUES ($1, $2, $3, $4, NULL, NULL, $5, NOW())`,
    [createObjId(), mbomHeaderObjid, changeType, description, userId],
  );
}

// 매퍼 partMng.getBOMTreeList search_type='working' 1:1.
// E-BOM 호환 — part_no 컬럼명 충돌 회피 위해 운영판처럼 V.PART_NO 는 PART_OBJID 로 alias.
const EBOM_WORKING_TREE_SQL = `
WITH RECURSIVE VIEW_BOM(
  BOM_REPORT_OBJID, OBJID, PARENT_OBJID, CHILD_OBJID,
  PARENT_PART_NO, PART_NO, LAST_PART_OBJID,
  QTY, ITEM_QTY, QTY_TEMP, REGDATE, SEQ, STATUS,
  PART_MNG_NO, PARENT_PART_MNG_NO, LEV, PATH, PATH2, CYCLE
) AS (
  SELECT A.BOM_REPORT_OBJID, A.OBJID, A.PARENT_OBJID, A.CHILD_OBJID,
         A.PARENT_PART_NO, A.PART_NO, A.LAST_PART_OBJID,
         A.QTY, A.ITEM_QTY, A.QTY_TEMP, A.REGDATE, A.SEQ, A.STATUS,
         (SELECT PART_NO FROM PART_MNG P WHERE P.OBJID::varchar = A.PART_NO)        AS PART_MNG_NO,
         (SELECT PART_NO FROM PART_MNG P WHERE P.OBJID::varchar = A.PARENT_PART_NO) AS PARENT_PART_MNG_NO,
         1,
         ARRAY [A.CHILD_OBJID::TEXT],
         ARRAY [LPAD(A.SEQ::TEXT, 10, '0')],
         FALSE
    FROM BOM_PART_QTY A
   WHERE 1=1
     AND (A.PARENT_OBJID IS NULL OR A.PARENT_OBJID = '')
     AND A.BOM_REPORT_OBJID = $1
     AND (A.STATUS NOT IN ('deleting', 'deleted') OR A.STATUS IS NULL)
  UNION ALL
  SELECT B.BOM_REPORT_OBJID, B.OBJID, B.PARENT_OBJID, B.CHILD_OBJID,
         B.PARENT_PART_NO, B.PART_NO, B.LAST_PART_OBJID,
         B.QTY, B.ITEM_QTY, B.QTY_TEMP, B.REGDATE, B.SEQ, B.STATUS,
         (SELECT PART_NO FROM PART_MNG P WHERE P.OBJID::varchar = B.PART_NO)        AS PART_MNG_NO,
         (SELECT PART_NO FROM PART_MNG P WHERE P.OBJID::varchar = B.PARENT_PART_NO) AS PARENT_PART_MNG_NO,
         LEV + 1,
         PATH || B.CHILD_OBJID::TEXT,
         PATH2 || LPAD(B.SEQ::TEXT, 10, '0'),
         B.PARENT_OBJID = ANY(PATH)
    FROM BOM_PART_QTY B
    JOIN VIEW_BOM ON B.PARENT_OBJID = VIEW_BOM.CHILD_OBJID
                 AND VIEW_BOM.BOM_REPORT_OBJID = B.BOM_REPORT_OBJID
                 AND (B.STATUS NOT IN ('deleting', 'deleted') OR B.STATUS IS NULL)
)
SELECT
  V.BOM_REPORT_OBJID  AS bom_report_objid,
  V.OBJID             AS objid,
  V.PARENT_OBJID      AS parent_objid,
  V.CHILD_OBJID       AS child_objid,
  V.PARENT_PART_NO    AS parent_part_no,
  V.PART_NO           AS part_objid,
  V.LAST_PART_OBJID   AS bom_last_part_objid,
  V.QTY               AS qty,
  V.ITEM_QTY          AS item_qty,
  (CASE WHEN V.STATUS = 'deploy' THEN V.QTY
        WHEN V.STATUS = 'beforeEdit' THEN V.QTY
        WHEN V.STATUS != 'editing' AND (V.QTY_TEMP IS NULL OR V.QTY_TEMP = '') THEN V.QTY
        ELSE COALESCE(V.QTY_TEMP, V.QTY) END) AS qty_temp,
  V.LEV               AS level,
  (SELECT COUNT(*) FROM BOM_PART_QTY WHERE PARENT_OBJID = V.CHILD_OBJID) AS sub_part_cnt,
  V.SEQ AS seq, V.STATUS AS status,
  P.OBJID  AS last_part_objid,
  P.PART_NAME AS part_name,
  P.PART_NO   AS part_no,
  (SELECT CODE_NAME FROM COMM_CODE CC WHERE CODE_ID = P.UNIT) AS unit_title,
  P.SPEC, P.MATERIAL, P.WEIGHT, P.REVISION, P.MAKER,
  (SELECT CODE_NAME FROM COMM_CODE CC WHERE CODE_ID = P.PART_TYPE) AS part_type_title,
  P.REMARK AS part_remark,
  P.THICKNESS, P.WIDTH, P.HEIGHT, P.OUT_DIAMETER, P.IN_DIAMETER, P.LENGTH,
  P.SOURCING_CODE, P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('3D_CAD'))         AS cu01_cnt,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_DRAWING_CAD')) AS cu02_cnt,
  (SELECT COUNT(1) FROM ATTACH_FILE_INFO F WHERE P.OBJID::varchar = F.TARGET_OBJID AND F.STATUS = 'Active' AND F.DOC_TYPE IN ('2D_PDF_CAD'))     AS cu03_cnt,
  -- E-BOM 분기는 생산정보가 없으므로 NULL 로 채워 SAVED 와 동일한 키셋 유지
  NULL::text AS supply_type, NULL::text AS make_or_buy,
  NULL::text AS raw_material_no, NULL::text AS raw_material_spec,
  NULL::text AS raw_material, NULL::text AS size,
  NULL::text AS processing_vendor, NULL::text AS processing_vendor_name,
  NULL::text AS processing_deadline, NULL::text AS grinding_deadline,
  NULL::numeric AS required_qty, NULL::numeric AS order_qty, NULL::numeric AS production_qty,
  NULL::numeric AS stock_qty, NULL::numeric AS shortage_qty,
  NULL::text AS vendor, NULL::text AS vendor_name,
  NULL::numeric AS unit_price, NULL::numeric AS processing_unit_price,
  NULL::numeric AS total_price, NULL::text AS currency,
  NULL::int AS lead_time, NULL::numeric AS min_order_qty,
  NULL::text AS writer, NULL::text AS regdate, NULL::text AS editer, NULL::text AS edit_date, NULL::text AS remark
FROM VIEW_BOM V
INNER JOIN PART_MNG P ON P.OBJID::varchar = COALESCE(V.LAST_PART_OBJID, V.PART_NO)
ORDER BY V.PATH2
`;
