// ============================================================
// 영업관리 > 주문서관리 (wace_plm 도메인 이식)
// 메인 테이블: contract_mgmt + contract_item (ORDER_* 컬럼) + contract_item_serial
// 견적관리와 동일 헤더 테이블을 공유. 차이점:
//  - 견적: estimate_template + contract_mgmt
//  - 주문: contract_mgmt(ORDER_*) + contract_item(ORDER_*)
// ============================================================

import { getPool } from "../database/db";
import { logger } from "../utils/logger";
import { AppError } from "../middleware/errorHandler";
import * as amaranth from "./amaranthApprovalClient";

export interface OrderListFilter {
  category_cd?: string;
  search_poNo?: string;
  customer_objid?: string;
  search_partObjId?: string;
  search_serialNo?: string;
  contract_result?: string;
  order_start_date?: string;
  order_end_date?: string;
  due_start_date?: string;
  due_end_date?: string;
  product?: string;
  area_cd?: string;
  paid_type?: string;
  contract_currency?: string;
}

export interface OrderItem {
  objid?: string;
  seq: number;
  part_objid: string;
  part_no: string;
  part_name: string;
  quantity?: number;
  due_date?: string;
  customer_request?: string;
  order_quantity?: string;
  order_unit_price?: string;
  order_supply_price?: string;
  order_vat?: string;
  order_total_amount?: string;
  cancel_qty?: string;
  return_reason?: string;
  product?: string;
  serials?: string[];
}

export interface OrderBody {
  // contract_mgmt 헤더 (wace estimateAndOrderRegistFormPopup 직접등록 통합폼 — G2)
  objid?: string;                    // 신규면 자동 생성
  contract_no?: string;
  category_cd?: string;
  customer_objid?: string;
  product?: string;
  area_cd?: string;
  paid_type?: string;
  contract_currency?: string;
  exchange_rate?: string;
  receipt_date?: string;             // 접수일 *
  order_date?: string;               // 발주일 * (wace G2 필수)
  req_del_date?: string;             // 요청납기
  po_no?: string;                    // 발주번호
  contract_result?: string;          // 수주상태
  approval_required?: string;        // 결재여부 'Y'|'N'
  pm_user_id?: string;
  customer_request?: string;
  shipping_method?: string;
  incoterms?: string;
  is_direct_order?: string;          // 'Y'면 직접등록 (견적관리 노출 X, 주문관리만 노출)
  // 라인
  items: OrderItem[];
}

function genObjid(prefix: string) {
  const ms = Date.now();
  const rand = Math.floor(Math.random() * 1000).toString().padStart(3, "0");
  return `${prefix}-${ms}-${rand}`;
}

// ─── 목록 ─────────────────────────────────────────────────────

export async function getList(filter: OrderListFilter) {
  const pool = getPool();
  const conditions: string[] = [];
  const params: any[] = [];
  let idx = 1;

  if (filter.customer_objid) { conditions.push(`T.CUSTOMER_OBJID = $${idx++}`); params.push(filter.customer_objid); }
  if (filter.category_cd) { conditions.push(`T.CATEGORY_CD = $${idx++}`); params.push(filter.category_cd); }
  if (filter.search_poNo) { conditions.push(`T.PO_NO ILIKE $${idx++}`); params.push(`%${filter.search_poNo}%`); }
  if (filter.contract_result) { conditions.push(`T.CONTRACT_RESULT = $${idx++}`); params.push(filter.contract_result); }
  if (filter.area_cd) { conditions.push(`T.AREA_CD = $${idx++}`); params.push(filter.area_cd); }
  if (filter.paid_type) { conditions.push(`T.PAID_TYPE = $${idx++}`); params.push(filter.paid_type); }
  if (filter.product) { conditions.push(`T.PRODUCT = $${idx++}`); params.push(filter.product); }
  if (filter.contract_currency) { conditions.push(`T.CONTRACT_CURRENCY = $${idx++}`); params.push(filter.contract_currency); }
  if (filter.order_start_date) { conditions.push(`T.ORDER_DATE >= $${idx++}`); params.push(filter.order_start_date); }
  if (filter.order_end_date) { conditions.push(`T.ORDER_DATE <= $${idx++}`); params.push(filter.order_end_date); }
  if (filter.search_partObjId) {
    conditions.push(`EXISTS (SELECT 1 FROM contract_item CI WHERE CI.contract_objid = T.OBJID AND CI.status='ACTIVE' AND CI.part_objid = $${idx++})`);
    params.push(filter.search_partObjId);
  }
  if (filter.search_serialNo) {
    conditions.push(`EXISTS (
      SELECT 1 FROM contract_item CI
      JOIN contract_item_serial CIS ON CI.objid=CIS.item_objid AND CIS.status='ACTIVE'
      WHERE CI.contract_objid=T.OBJID AND CI.status='ACTIVE'
        AND UPPER(CIS.serial_no) LIKE UPPER($${idx++})
    )`);
    params.push(`%${filter.search_serialNo}%`);
  }
  if (filter.due_start_date) {
    conditions.push(`(SELECT MIN(CI.due_date) FROM contract_item CI WHERE CI.contract_objid=T.OBJID AND CI.status='ACTIVE' AND CI.due_date IS NOT NULL AND CI.due_date != '') >= $${idx++}`);
    params.push(filter.due_start_date);
  }
  if (filter.due_end_date) {
    conditions.push(`(SELECT MIN(CI.due_date) FROM contract_item CI WHERE CI.contract_objid=T.OBJID AND CI.status='ACTIVE' AND CI.due_date IS NOT NULL AND CI.due_date != '') <= $${idx++}`);
    params.push(filter.due_end_date);
  }

  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";

  const sql = `
    SELECT
      T.OBJID
      ,T.CONTRACT_NO
      ,T.CATEGORY_CD
      ,CC_CAT.code_name          AS CATEGORY_NAME
      ,T.CUSTOMER_OBJID
      ,C.customer_name           AS CUSTOMER_NAME
      ,T.PRODUCT
      ,COALESCE(CI_AGG.product_summary, CC_PRD.code_name) AS PRODUCT_NAME
      ,T.AREA_CD
      ,CC_AREA.code_name         AS AREA_NAME
      ,T.PAID_TYPE
      ,CASE WHEN T.PAID_TYPE='paid' THEN '유상' WHEN T.PAID_TYPE='free' THEN '무상' ELSE T.PAID_TYPE END AS PAID_TYPE_NAME
      ,T.CONTRACT_CURRENCY
      ,CC_CUR.code_name          AS CONTRACT_CURRENCY_NAME
      ,T.EXCHANGE_RATE
      ,T.PO_NO
      ,T.ORDER_DATE              AS ORDER_DATE
      ,T.RECEIPT_DATE
      ,T.REQ_DEL_DATE
      ,T.CONTRACT_RESULT
      ,CC_RES.code_name          AS CONTRACT_RESULT_NAME
      ,T.ORDER_SUPPLY_PRICE      AS ORDER_SUPPLY_PRICE_SUM
      ,T.ORDER_VAT               AS ORDER_VAT_SUM
      ,T.ORDER_TOTAL_AMOUNT      AS ORDER_TOTAL_AMOUNT_SUM
      ,CASE
         WHEN T.EXCHANGE_RATE IS NOT NULL AND T.EXCHANGE_RATE != ''
              AND REPLACE(T.EXCHANGE_RATE, ',', '') ~ '^[0-9]+\\.{0,1}[0-9]*$'
              AND CAST(REPLACE(T.EXCHANGE_RATE, ',', '') AS NUMERIC) != 0
         THEN ROUND(
                CAST(REPLACE(COALESCE(T.ORDER_TOTAL_AMOUNT, '0'), ',', '') AS NUMERIC)
                * CAST(REPLACE(T.EXCHANGE_RATE, ',', '') AS NUMERIC), 2)
         ELSE CAST(REPLACE(COALESCE(T.ORDER_TOTAL_AMOUNT, '0'), ',', '') AS NUMERIC)
       END                       AS ORDER_TOTAL_AMOUNT_KRW
      ,T.CUSTOMER_REQUEST        AS CUSTOMER_REQUEST
      ,T.WRITER
      ,U_WR.user_name            AS WRITER_NAME
      ,T.PM_USER_ID
      ,U_PM.user_name            AS PM_USER_NAME
      ,T.REGDATE
      ,COALESCE(CI_AGG.order_quantity_sum, 0) AS ORDER_QUANTITY
      ,COALESCE(CI_AGG.cancel_qty_sum, 0)     AS CANCEL_QTY_SUM
      ,COALESCE(CI_AGG.has_order_data, 0)     AS HAS_ORDER_DATA
      ,CI_AGG.earliest_due_date               AS EARLIEST_DUE_DATE
      ,COALESCE(CI_AGG.other_due_date_count, 0) AS OTHER_DUE_DATE_COUNT
      ,CASE
         WHEN COALESCE(CI_AGG.item_count, 0) = 1 THEN CI_AGG.first_part_name
         WHEN COALESCE(CI_AGG.item_count, 0) > 1 THEN CI_AGG.first_part_name || ' 외 ' || (CI_AGG.item_count - 1) || '건'
         ELSE ''
       END AS ITEM_SUMMARY
      ,CASE
         WHEN COALESCE(CI_AGG.item_count, 0) = 1 THEN CI_AGG.first_part_no
         WHEN COALESCE(CI_AGG.item_count, 0) > 1 THEN CI_AGG.first_part_no || ' 외 ' || (CI_AGG.item_count - 1) || '건'
         ELSE T.PART_NO
       END AS PART_NO
      ,CASE
         WHEN COALESCE(SER_AGG.serial_count, 0) = 1 THEN SER_AGG.first_serial_no
         WHEN COALESCE(SER_AGG.serial_count, 0) > 1 THEN SER_AGG.first_serial_no || ' 외 ' || (SER_AGG.serial_count - 1) || '건'
         ELSE T.SERIAL_NO
       END AS SERIAL_NO
      /* G4/G11 — 수주 결재상태 (wace contractMgmt.xml:523~530 1:1)
         target_type='CONTRACT_ORDER' amaranth_approval 매핑 결과를 한글 라벨/원본 상태로 노출 */
      ,CASE
         WHEN AMR_ORDER.status = 'complete'  THEN '결재완료'
         WHEN AMR_ORDER.status = 'inProcess' THEN '결재중'
         WHEN AMR_ORDER.status = 'reject'    THEN '반려'
         WHEN AMR_ORDER.status = 'create'    THEN '작성중'
         ELSE ''
       END AS ORDER_APPR_STATUS
      ,COALESCE(AMR_ORDER.status, '') AS ORDER_AMARANTH_STATUS
      ,COALESCE(AMR_ORDER.status, '') AS AMARANTH_STATUS
      ,0            AS CU01_CNT
      ,T.IS_DIRECT_ORDER         AS IS_DIRECT_ORDER
    FROM contract_mgmt T
    LEFT JOIN customer_mng C
      ON C.customer_code = CASE WHEN T.CUSTOMER_OBJID LIKE 'C_%' THEN substring(T.CUSTOMER_OBJID, 3) ELSE T.CUSTOMER_OBJID END
    LEFT JOIN user_info U_WR ON U_WR.user_id = T.WRITER
    LEFT JOIN user_info U_PM ON U_PM.user_id = T.PM_USER_ID
    /* 공통코드 한글명 */
    LEFT JOIN comm_code CC_CAT  ON CC_CAT.code_id  = T.CATEGORY_CD       AND CC_CAT.status='active'
    LEFT JOIN comm_code CC_AREA ON CC_AREA.code_id = T.AREA_CD           AND CC_AREA.status='active'
    LEFT JOIN comm_code CC_PRD  ON CC_PRD.code_id  = T.PRODUCT           AND CC_PRD.status='active'
    LEFT JOIN comm_code CC_CUR  ON CC_CUR.code_id  = T.CONTRACT_CURRENCY AND CC_CUR.status='active'
    LEFT JOIN comm_code CC_RES  ON CC_RES.code_id  = T.CONTRACT_RESULT   AND CC_RES.status='active'
    LEFT JOIN (
      SELECT
        CI.contract_objid,
        COUNT(*) AS item_count,
        (array_agg(COALESCE(PM.part_name, CI.part_name) ORDER BY CI.seq))[1] AS first_part_name,
        (array_agg(COALESCE(PM.part_no, CI.part_no)  ORDER BY CI.seq))[1] AS first_part_no,
        MIN(CASE WHEN CI.due_date IS NOT NULL AND CI.due_date != '' THEN CI.due_date END) AS earliest_due_date,
        GREATEST(COUNT(CASE WHEN CI.due_date IS NOT NULL AND CI.due_date != '' THEN 1 END) - 1, 0) AS other_due_date_count,
        COALESCE(SUM(CAST(REPLACE(NULLIF(CI.order_quantity, ''), ',', '') AS NUMERIC)), 0) AS order_quantity_sum,
        COALESCE(SUM(CASE WHEN CI.cancel_qty IS NOT NULL AND CI.cancel_qty != '' AND CI.cancel_qty != '0'
                          THEN CAST(CI.cancel_qty AS NUMERIC) END), 0) AS cancel_qty_sum,
        COUNT(CASE WHEN CI.order_quantity IS NOT NULL AND CI.order_quantity != '' AND CI.order_quantity != '0' THEN 1 END) AS has_order_data,
        STRING_AGG(DISTINCT CC_PRDI.code_name, ', ') FILTER (WHERE CC_PRDI.code_name IS NOT NULL) AS product_summary
      FROM contract_item CI
      LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid
      LEFT JOIN comm_code CC_PRDI ON CC_PRDI.code_id = CI.product AND CC_PRDI.status='active'
      WHERE CI.status='ACTIVE'
      GROUP BY CI.contract_objid
    ) CI_AGG ON CI_AGG.contract_objid = T.OBJID
    LEFT JOIN (
      SELECT CI.contract_objid,
             (array_agg(CIS.serial_no ORDER BY CI.seq, CIS.seq))[1] AS first_serial_no,
             COUNT(*) AS serial_count
      FROM contract_item CI
      JOIN contract_item_serial CIS ON CI.objid=CIS.item_objid AND CIS.status='ACTIVE'
      WHERE CI.status='ACTIVE' AND CIS.serial_no IS NOT NULL AND CIS.serial_no != ''
      GROUP BY CI.contract_objid
    ) SER_AGG ON SER_AGG.contract_objid = T.OBJID
    /* G4/G11 — 아마란스 결재(수주) — wace 1:1, target_objid는 VARCHAR */
    LEFT JOIN amaranth_approval AMR_ORDER
           ON AMR_ORDER.target_objid = T.OBJID
          AND AMR_ORDER.target_type = 'CONTRACT_ORDER'
    ${where}
    ORDER BY T.regdate DESC
  `;

  const res = await pool.query(sql, params);
  logger.info("주문서 목록 조회", { count: res.rowCount, filter });
  return res.rows;
}

// ─── 단건 ─────────────────────────────────────────────────────

export async function getById(objid: string) {
  const pool = getPool();
  const headerRes = await pool.query(
    `SELECT T.*, C.customer_name, U_WR.user_name AS writer_name, U_PM.user_name AS pm_user_name
     FROM contract_mgmt T
     LEFT JOIN customer_mng C
       ON C.customer_code = CASE WHEN T.customer_objid LIKE 'C_%' THEN substring(T.customer_objid, 3) ELSE T.customer_objid END
     LEFT JOIN user_info U_WR ON U_WR.user_id = T.writer
     LEFT JOIN user_info U_PM ON U_PM.user_id = T.pm_user_id
     WHERE T.objid = $1`, [objid],
  );
  if (headerRes.rowCount === 0) return null;

  const itemsRes = await pool.query(
    `SELECT CI.*, PM.part_name AS master_item_name
     FROM contract_item CI
     LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid
     WHERE CI.contract_objid = $1 AND CI.status = 'ACTIVE'
     ORDER BY CI.seq`, [objid],
  );

  const itemIds = itemsRes.rows.map((r: any) => r.objid);
  let serials: any[] = [];
  if (itemIds.length > 0) {
    const sRes = await pool.query(
      `SELECT objid, item_objid, seq, serial_no FROM contract_item_serial
       WHERE item_objid = ANY($1::varchar[]) AND status = 'ACTIVE'
       ORDER BY item_objid, seq`,
      [itemIds],
    );
    serials = sRes.rows;
  }

  const items = itemsRes.rows.map((it: any) => ({
    ...it,
    serials: serials.filter((s) => s.item_objid === it.objid).map((s) => s.serial_no),
  }));
  return { ...headerRes.rows[0], items };
}

// ─── 주문서 뷰 (wace orderFormView 대응) ──────────────────────
// wace 패턴: getOrderFormInfo(헤더 + 거래처) + getOrderFormItems(라인) 두 쿼리.

export async function getOrderFormView(objid: string) {
  const pool = getPool();
  const headerSql = `
    SELECT
      T.objid                              AS objid,
      T.contract_no                        AS contract_no,
      T.po_no                              AS po_no,
      T.order_date                         AS order_date,
      T.customer_objid                     AS customer_objid,
      CC_CAT.code_name                     AS category_name,
      CC_CUR.code_name                     AS currency_name,
      T.contract_currency                  AS currency_code,
      T.exchange_rate                      AS exchange_rate,
      T.order_supply_price                 AS order_supply_price,
      T.order_vat                          AS order_vat,
      T.order_total_amount                 AS order_total_amount,
      CASE
        WHEN T.paid_type = 'paid' THEN '부가세별도'
        WHEN T.paid_type = 'free' THEN '부가세미포함'
        ELSE ''
      END                                  AS vat_note,
      T.customer_request                   AS customer_request,
      C.customer_name                      AS client_nm,
      C.business_number                    AS client_bus_reg_no,
      COALESCE(C.ceo_name, C.representative_name)  AS client_ceo_nm,
      C.address                            AS client_addr,
      C.biz_condition                      AS client_bus_type,
      C.biz_item                           AS client_bus_item,
      COALESCE(C.tel, C.phone, C.contact_phone)    AS client_tel_no,
      COALESCE(C.fax_no, C.fax)            AS client_fax_no,
      COALESCE(C.email, C.charge_email)    AS client_email,
      TRIM(BOTH FROM COALESCE(U_WR.dept_name, '') || ' ' || COALESCE(U_WR.user_name, T.writer)) AS writer_name,
      U_WR.tel                             AS writer_contact,
      TO_CHAR(T.regdate, 'YYYY-MM-DD HH24:MI:SS') AS reg_datetime
    FROM contract_mgmt T
    LEFT JOIN customer_mng C
      ON C.customer_code = CASE WHEN T.customer_objid LIKE 'C_%' THEN substring(T.customer_objid, 3) ELSE T.customer_objid END
    LEFT JOIN user_info U_WR ON U_WR.user_id = T.writer
    LEFT JOIN comm_code CC_CAT ON CC_CAT.code_id = T.category_cd       AND CC_CAT.status='active'
    LEFT JOIN comm_code CC_CUR ON CC_CUR.code_id = T.contract_currency AND CC_CUR.status='active'
    WHERE T.objid = $1
  `;
  const headerRes = await pool.query(headerSql, [objid]);
  if (headerRes.rowCount === 0) return null;

  const itemsSql = `
    SELECT
      CI.seq,
      COALESCE(PM.part_no, CI.part_no)   AS part_no,
      COALESCE(PM.part_name,   CI.part_name) AS part_name,
      IT.size                                AS spec,
      CC_UNIT.code_name                      AS unit_name,
      IT.unit                                AS unit_code,
      CI.due_date                            AS due_date,
      CI.order_quantity                      AS order_quantity,
      CI.order_unit_price                    AS order_unit_price,
      CI.order_supply_price                  AS order_supply_price,
      CI.order_vat                           AS order_vat,
      CI.order_total_amount                  AS order_total_amount
    FROM contract_item CI
    LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid
    LEFT JOIN comm_code CC_UNIT ON CC_UNIT.code_id = IT.unit AND CC_UNIT.status='active'
    WHERE CI.contract_objid = $1 AND CI.status = 'ACTIVE'
    ORDER BY CI.seq
  `;
  const itemsRes = await pool.query(itemsSql, [objid]);

  logger.info("주문서 뷰 조회", { objid, itemCount: itemsRes.rowCount });
  return { info: headerRes.rows[0], items: itemsRes.rows };
}

// ─── 영업번호 채번 ─────────────────────────────────────────────

// 영업번호 채번 — wace 운영 패턴: {YY}C-{NNNN}
//   YY = 년도 뒷 2자리 / C = 고정 / NNNN = 같은 prefix 안에서 MAX(NNNN)+1, 4자리 zero-pad
//   운영 데이터 90건 모두 이 형식 (예: 26C-0801, 26C-0800, ...)
export async function generateContractNo(): Promise<string> {
  const pool = getPool();
  const yy = String(new Date().getFullYear()).slice(-2);
  const prefix = `${yy}C-`;
  const res = await pool.query(
    `SELECT contract_no FROM contract_mgmt
     WHERE contract_no LIKE $1
     ORDER BY contract_no DESC LIMIT 1`, [`${prefix}%`],
  );
  let seq = 1;
  if (res.rowCount && res.rowCount > 0) {
    const last = res.rows[0].contract_no as string;
    const lastSeq = parseInt(last.replace(prefix, ""), 10);
    if (!isNaN(lastSeq)) seq = lastSeq + 1;
  }
  return `${prefix}${String(seq).padStart(4, "0")}`;
}

// ─── 생성/수정 공용 ───────────────────────────────────────────

async function upsertItems(client: any, contractObjid: string, items: OrderItem[], userId: string) {
  // 기존 라인 status='INACTIVE' 처리
  await client.query(
    `UPDATE contract_item SET status='INACTIVE', chgdate=NOW(), chg_user_id=$2 WHERE contract_objid=$1`,
    [contractObjid, userId],
  );
  for (const item of items) {
    const itemObjid = item.objid || genObjid("CI");
    await client.query(
      `INSERT INTO contract_item (
         objid, contract_objid, seq, part_objid, part_no, part_name,
         quantity, due_date, customer_request,
         order_quantity, order_unit_price, order_supply_price, order_vat, order_total_amount,
         cancel_qty, return_reason, product,
         regdate, writer, status
       ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,NOW(),$18,'ACTIVE')
       ON CONFLICT (objid) DO UPDATE SET
         seq=EXCLUDED.seq,
         part_objid=EXCLUDED.part_objid, part_no=EXCLUDED.part_no, part_name=EXCLUDED.part_name,
         quantity=EXCLUDED.quantity, due_date=EXCLUDED.due_date, customer_request=EXCLUDED.customer_request,
         order_quantity=EXCLUDED.order_quantity, order_unit_price=EXCLUDED.order_unit_price,
         order_supply_price=EXCLUDED.order_supply_price, order_vat=EXCLUDED.order_vat,
         order_total_amount=EXCLUDED.order_total_amount, cancel_qty=EXCLUDED.cancel_qty,
         return_reason=EXCLUDED.return_reason, product=EXCLUDED.product,
         chgdate=NOW(), chg_user_id=$18, status='ACTIVE'`,
      [
        itemObjid, contractObjid, item.seq, item.part_objid, item.part_no, item.part_name,
        item.quantity ?? 1, item.due_date || null, item.customer_request || null,
        item.order_quantity || null, item.order_unit_price || null, item.order_supply_price || null,
        item.order_vat || null, item.order_total_amount || null,
        item.cancel_qty || null, item.return_reason || null, item.product || null,
        userId,
      ],
    );
    // 시리얼 재구성
    await client.query(`UPDATE contract_item_serial SET status='INACTIVE' WHERE item_objid=$1`, [itemObjid]);
    const serials = item.serials ?? [];
    for (let i = 0; i < serials.length; i++) {
      await client.query(
        `INSERT INTO contract_item_serial (objid, item_objid, seq, serial_no, regdate, writer, status)
         VALUES ($1, $2, $3, $4, NOW(), $5, 'ACTIVE')
         ON CONFLICT (item_objid, serial_no) DO UPDATE SET status='ACTIVE', seq=EXCLUDED.seq`,
        [genObjid("CIS"), itemObjid, i + 1, serials[i], userId],
      );
    }
  }
}

export async function create(userId: string, body: OrderBody) {
  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    const objid = body.objid || genObjid("CM");
    const contractNo = body.contract_no || (await generateContractNo());

    // 헤더 ORDER_* 합계 계산
    const items = body.items ?? [];
    const sum = (key: keyof OrderItem) => items.reduce((s, it) => {
      const v = parseFloat(String(it[key] ?? "0").replace(/,/g, ""));
      return s + (isNaN(v) ? 0 : v);
    }, 0);

    // wace G2 패턴: 주문관리 등록은 직접등록 통합폼 — is_direct_order='Y' (견적관리에 노출 X)
    const isDirectOrder = body.is_direct_order || "Y";

    await client.query(
      `INSERT INTO contract_mgmt (
         objid, contract_no, category_cd, customer_objid, product, area_cd, paid_type,
         contract_currency, exchange_rate, receipt_date, order_date, req_del_date,
         po_no, contract_result, approval_required, pm_user_id, customer_request,
         shipping_method, incoterms, is_direct_order,
         order_supply_price, order_vat, order_total_amount,
         writer, regdate
       ) 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,NOW()
       )`,
      [
        objid, contractNo, body.category_cd || null, body.customer_objid || null,
        body.product || null, body.area_cd || null, body.paid_type || null,
        body.contract_currency || null, body.exchange_rate || null,
        body.receipt_date || null, body.order_date || null, body.req_del_date || null,
        body.po_no || null, body.contract_result || null, body.approval_required || "N",
        body.pm_user_id || null, body.customer_request || null,
        body.shipping_method || null, body.incoterms || null, isDirectOrder,
        String(sum("order_supply_price")), String(sum("order_vat")), String(sum("order_total_amount")),
        userId,
      ],
    );

    await upsertItems(client, objid, items, userId);
    await client.query("COMMIT");
    logger.info("주문서 등록", { objid, contractNo, items: items.length });
    return { objid, contract_no: contractNo };
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally { client.release(); }
}

export async function update(userId: string, objid: string, body: OrderBody) {
  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    const items = body.items ?? [];
    const sum = (key: keyof OrderItem) => items.reduce((s, it) => {
      const v = parseFloat(String(it[key] ?? "0").replace(/,/g, ""));
      return s + (isNaN(v) ? 0 : v);
    }, 0);

    await client.query(
      `UPDATE contract_mgmt SET
         category_cd=$2, customer_objid=$3, product=$4, area_cd=$5, paid_type=$6,
         contract_currency=$7, exchange_rate=$8, receipt_date=$9, order_date=$10,
         req_del_date=$11, po_no=$12, contract_result=$13, approval_required=$14, pm_user_id=$15,
         customer_request=$16, shipping_method=$17, incoterms=$18,
         order_supply_price=$19, order_vat=$20, order_total_amount=$21,
         chg_user_id=$22
       WHERE objid=$1`,
      [
        objid, body.category_cd || null, body.customer_objid || null,
        body.product || null, body.area_cd || null, body.paid_type || null,
        body.contract_currency || null, body.exchange_rate || null,
        body.receipt_date || null, body.order_date || null, body.req_del_date || null,
        body.po_no || null, body.contract_result || null, body.approval_required || "N",
        body.pm_user_id || null, body.customer_request || null,
        body.shipping_method || null, body.incoterms || null,
        String(sum("order_supply_price")), String(sum("order_vat")), String(sum("order_total_amount")),
        userId,
      ],
    );

    await upsertItems(client, objid, items, userId);
    await client.query("COMMIT");
    logger.info("주문서 수정", { objid, items: items.length });
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally { client.release(); }
}

// G8 — 프로젝트가 생성된 contract는 견적/주문 모두 삭제 차단.
// wace deleteContractMngInfo:794-808 + mapper getProjectListBycontractObjid:3909 1:1.
// 원본 매퍼는 status 필터 없이 단순 contract_objid 매칭 → 그대로 따름.
// salesEstimateService.remove에서도 import해서 사용.
export async function assertNoProjectExists(client: any, contractObjid: string) {
  const r = await client.query(
    `SELECT 1 FROM project_mgmt WHERE contract_objid=$1 LIMIT 1`,
    [contractObjid],
  );
  if (r.rowCount > 0) {
    throw new AppError("프로젝트가 생성된 건은 삭제할 수 없습니다.", 409);
  }
}

export async function remove(objid: string) {
  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    // G8 guard
    await assertNoProjectExists(client, objid);
    await client.query(`DELETE FROM contract_mgmt WHERE objid=$1`, [objid]);
    await client.query("COMMIT");
    // contract_item / contract_item_serial은 ON DELETE CASCADE.
    // estimate_template (contract_objid 참조)는 FK 없어 남을 수 있음 — 별도 정책 결정 영역.
    logger.info("주문서 삭제", { objid });
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}

// ─── 수주확정 → 프로젝트 자동생성 (wace ContractMgmtService.updateOrderStatus 이식) ──
// 원본: wace_plm/src/com/pms/salesmgmt/service/ContractMgmtService.java:2987~3113
//       + wace_plm/src/com/pms/mapper/project.xml:7415~7618 (createProject)
// 트리거: contract_result='0000964'(수주) 또는 '0000968'(수주FCST)
// 룰: project_no = {주문유형}-{제품구분}-{YYMMDD}-{NNN}
//     주문유형(category code_name): 오버홀=O 개조=M 개발=D 견적=Q 수리=R 판매=S 그외=T
//     제품구분(product code_name):  Machine=MC A/S=AS D/S=DS B/S=BS C/T=CT A/C=AC W/M=WM 기타=기타 그외=replace('/', '')
//     순번: 같은 prefix-prefix-YYMMDD-% 안에서 MAX(NNN)+1, 없으면 001
// Machine(0000928): hasProject=false면 quantity 만큼 N회 INSERT (각 quantity=1)

const PROJECT_TRIGGER_RESULTS = ["0000964", "0000968"] as const;
const PRODUCT_MACHINE = "0000928";

async function generateProjectNo(client: any, categoryCd: string | null, productCd: string | null): Promise<string> {
  const sql = `
    WITH meta AS (
      SELECT
        CASE CC_CAT.code_name
          WHEN '오버홀' THEN 'O' WHEN '개조' THEN 'M' WHEN '개발' THEN 'D'
          WHEN '견적' THEN 'Q'   WHEN '수리' THEN 'R' WHEN '판매' THEN 'S'
          ELSE 'T'
        END AS cat_abbr,
        CASE CC_PRD.code_name
          WHEN 'Machine' THEN 'MC' WHEN 'A/S' THEN 'AS' WHEN 'D/S' THEN 'DS'
          WHEN 'B/S' THEN 'BS'     WHEN 'C/T' THEN 'CT' WHEN 'A/C' THEN 'AC'
          WHEN 'W/M' THEN 'WM'     WHEN '기타' THEN '기타'
          ELSE REPLACE(COALESCE(CC_PRD.code_name, ''), '/', '')
        END AS prd_abbr,
        TO_CHAR(CURRENT_DATE, 'YYMMDD') AS ymd
      FROM (SELECT 1) X
      LEFT JOIN comm_code CC_CAT ON CC_CAT.code_id = $1 AND CC_CAT.status='active'
      LEFT JOIN comm_code CC_PRD ON CC_PRD.code_id = $2 AND CC_PRD.status='active'
    )
    SELECT
      m.cat_abbr || '-' || m.prd_abbr || '-' || m.ymd || '-' ||
      LPAD(
        COALESCE((
          SELECT MAX(SUBSTRING(project_no FROM '\\d{3}$')::int) + 1
          FROM project_mgmt
          WHERE project_no LIKE m.cat_abbr || '-' || m.prd_abbr || '-' || m.ymd || '-%'
        ), 1)::text, 3, '0'
      ) AS project_no
    FROM meta m
  `;
  const res = await client.query(sql, [categoryCd, productCd]);
  return res.rows[0]?.project_no || "";
}

async function insertProjectFromContract(
  client: any,
  contractObjid: string,
  item: any,
  productCd: string | null,
  quantity: string,
  projectNo: string,
  userId: string,
) {
  const newObjid = genObjid("PJ");
  const sql = `
    INSERT INTO project_mgmt (
      objid, contract_objid, category_cd, customer_objid, product,
      customer_project_name, status_cd, due_date, location, setup,
      facility, facility_qty, facility_type, facility_depth, production_no,
      bus_cal_cd, category1_cd, chg_user_id, plan_date, complete_date,
      result_cd, project_no, pm_user_id, contract_price, contract_price_currency,
      contract_currency, regdate, writer, contract_no, customer_equip_name,
      req_del_date, contract_del_date, contract_company, contract_date, po_no,
      manufacture_plant, contract_result, project_name, spec_user_id, spec_plan_date,
      spec_comp_date, spec_result_cd, est_plan_date, est_user_id, est_comp_date,
      est_result_cd, area_cd, mechanical_type, overhaul_order, is_temp,
      part_objid, part_no, part_name, quantity, contract_item_objid
    )
    SELECT
      $1::varchar, $2::varchar, T.category_cd, T.customer_objid, COALESCE($3::varchar, T.product),
      T.customer_project_name, T.status_cd, $4::varchar, T.location, T.setup,
      T.facility, T.facility_qty, T.facility_type, T.facility_depth, T.production_no,
      T.bus_cal_cd, T.category1_cd, $5::varchar, T.plan_date, T.complete_date,
      T.result_cd, $6::varchar, T.pm_user_id, NULL, NULL,
      T.contract_currency, NOW(), T.writer, T.contract_no, T.customer_equip_name,
      T.req_del_date, T.contract_del_date, T.contract_company, T.contract_date, T.po_no,
      T.manufacture_plant, T.contract_result, NULL, T.spec_user_id, T.spec_plan_date,
      T.spec_comp_date, T.spec_result_cd, T.est_plan_date, T.est_user_id, T.est_comp_date,
      T.est_result_cd, T.area_cd, T.mechanical_type, NULL, '1',
      $7::varchar, $8::varchar, $9::varchar, $10::varchar, $11::varchar
    FROM contract_mgmt T WHERE T.objid = $2::varchar
  `;
  await client.query(sql, [
    newObjid,
    contractObjid,
    productCd,
    item.due_date || null,
    userId,
    projectNo,
    item.part_objid || null,
    item.part_no || null,
    item.part_name || null,
    quantity,
    item.objid || null,
  ]);
  logger.info("[수주확정] 프로젝트 생성", { contractObjid, partObjid: item.part_objid, projectNo, quantity });
}

async function updateProjectFromContract(
  client: any,
  contractObjid: string,
  item: any,
  isMachine: boolean,
  quantity: string,
  productCd: string | null,
  userId: string,
) {
  // wace ModifyProjectByContract: SET DUE_DATE/QUANTITY/PRODUCT 등 (Machine은 quantity 미변경)
  const setParts: string[] = [];
  const params: any[] = [];
  let idx = 1;
  if (item.due_date) { setParts.push(`due_date=$${idx++}`); params.push(item.due_date); }
  if (!isMachine) { setParts.push(`quantity=$${idx++}`); params.push(quantity); }
  if (productCd) { setParts.push(`product=$${idx++}`); params.push(productCd); }
  setParts.push(`chg_user_id=$${idx++}`); params.push(userId);

  // contract_item_objid 우선 매칭, 없으면 part_objid fallback (wace choose 패턴)
  const sql = `UPDATE project_mgmt SET ${setParts.join(", ")}
    WHERE contract_objid = $${idx++}
      AND (contract_item_objid = $${idx++}
           OR (contract_item_objid IS NULL AND part_objid = $${idx++}))`;
  params.push(contractObjid, item.objid, item.part_objid);
  await client.query(sql, params);
  logger.info("[수주확정] 프로젝트 업데이트", { contractObjid, partObjid: item.part_objid, isMachine });
}

async function createProjectsFromContract(client: any, contractObjid: string, userId: string) {
  // 1) 기존 프로젝트 존재 여부
  const existsRes = await client.query(
    `SELECT objid FROM project_mgmt WHERE contract_objid = $1 LIMIT 1`,
    [contractObjid],
  );
  const hasProject = (existsRes.rowCount ?? 0) > 0;

  // 2) 헤더 기본 정보
  const headerRes = await client.query(
    `SELECT category_cd, product FROM contract_mgmt WHERE objid = $1`,
    [contractObjid],
  );
  if (headerRes.rowCount === 0) return;
  const header = headerRes.rows[0];

  // 3) 라인 조회 (item_info 마스터 우선 fallback — wace getContractItems 패턴)
  const itemsRes = await client.query(
    `SELECT
       CI.objid, CI.part_objid,
       COALESCE(PM.part_no, CI.part_no) AS part_no,
       COALESCE(PM.part_name, CI.part_name) AS part_name,
       CI.quantity, CI.order_quantity, CI.due_date, CI.product
     FROM contract_item CI
     LEFT JOIN part_mng PM ON PM.objid::varchar = CI.part_objid
     WHERE CI.contract_objid = $1 AND CI.status = 'ACTIVE'
     ORDER BY CI.seq`,
    [contractObjid],
  );

  if (itemsRes.rowCount === 0) {
    logger.warn("[수주확정] contract_item 없음 — 프로젝트 생성 스킵", { contractObjid });
    return;
  }

  let createdCount = 0;
  for (const item of itemsRes.rows) {
    const itemProduct: string | null = item.product || header.product || null;
    const isMachine = itemProduct === PRODUCT_MACHINE;

    // 수량: order_quantity 우선, 없으면 quantity, 둘 다 없으면 1
    const rawQty = String(item.order_quantity ?? item.quantity ?? "1").replace(/[^0-9.]/g, "");
    let qtyNum = parseFloat(rawQty);
    if (!Number.isFinite(qtyNum) || qtyNum <= 0) qtyNum = 1;
    const itemQuantity = Math.floor(qtyNum);

    if (hasProject) {
      // 존재하면 1회 UPDATE만 (Machine이면 quantity 미변경)
      const updQty = isMachine ? "1" : String(itemQuantity);
      await updateProjectFromContract(client, contractObjid, item, isMachine, updQty, itemProduct, userId);
    } else {
      // 신규: Machine이면 quantity만큼 반복(각 1), 그외 1회(원래 수량)
      const loopCount = isMachine ? itemQuantity : 1;
      const insertQty = isMachine ? "1" : String(itemQuantity);
      for (let q = 0; q < loopCount; q++) {
        const projectNo = await generateProjectNo(client, header.category_cd, itemProduct);
        await insertProjectFromContract(client, contractObjid, item, itemProduct, insertQty, projectNo, userId);
        createdCount++;
      }
    }
  }

  logger.info("[수주확정] 프로젝트 처리 완료", {
    contractObjid, hasProject, items: itemsRes.rowCount, created: createdCount,
  });
}

// ─── 수주 상태 변경 ────────────────────────────────────────────

// ─── 수주취소 (라인별 cancel_qty 입력) — wace saveOrderCancelQty 이식 ──────
// 원본: ContractMgmtService.java:3932~4011, contractMgmt.xml:5292~5299
// 룰:
//   - cancel_qty 비었거나 0 → 빈값으로 초기화 (UPDATE cancel_qty='')
//   - 음수 → 에러 ("수주취소 수량은 0 이상이어야 합니다")
//   - order_qty 이상 → 에러 + rollback ("수주취소 수량은 수주수량(N)보다 적어야 합니다") — 전체 취소 불가
//   - 정상 → UPDATE cancel_qty=값
// 응답: { result: 'true'|'false', msg }

export interface CancelQtyEntry {
  itemObjId: string;
  cancelQty: string | number;
  orderQty: string | number;
}

export async function saveOrderCancelQty(userId: string, entries: CancelQtyEntry[]):
  Promise<{ result: "true" | "false"; msg: string }>
{
  if (!entries || entries.length === 0) {
    return { result: "false", msg: "취소 수량 정보가 없습니다." };
  }

  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    for (const e of entries) {
      const cancelStr = String(e.cancelQty ?? "").trim();
      const orderStr = String(e.orderQty ?? "").trim();

      // 빈값 또는 0 → 초기화
      if (cancelStr === "" || cancelStr === "0") {
        await client.query(
          `UPDATE contract_item SET cancel_qty='', chgdate=NOW(), chg_user_id=$2 WHERE objid=$1`,
          [e.itemObjId, userId],
        );
        continue;
      }

      const cancelInt = parseInt(cancelStr, 10);
      const orderInt = parseInt(orderStr, 10);

      if (Number.isNaN(cancelInt) || cancelInt < 0) {
        await client.query("ROLLBACK");
        return { result: "false", msg: "수주취소 수량은 0 이상이어야 합니다." };
      }
      if (cancelInt >= orderInt) {
        await client.query("ROLLBACK");
        return { result: "false", msg: `수주취소 수량은 수주수량(${orderInt})보다 적어야 합니다.` };
      }

      await client.query(
        `UPDATE contract_item SET cancel_qty=$2, chgdate=NOW(), chg_user_id=$3 WHERE objid=$1`,
        [e.itemObjId, cancelStr, userId],
      );
    }

    await client.query("COMMIT");
    logger.info("수주취소 수량 저장", { count: entries.length });
    return { result: "true", msg: "수주취소 수량이 저장되었습니다." };
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}

// G9 — 수주복사 (wace copyEstimateAndOrderInfo 1:1)
// contract_mgmt 헤더 + contract_item 라인 + contract_item_serial 통째로 새 영업번호로 복제.
// 복사본은 contract_result='', is_direct_order='Y'로 시작 (주문관리에만 노출).
// 매퍼 출처: contractMgmt.xml copyContractMgmt / copyContractItems / copyContractItemSerials / getNextContractNo.
export async function copyOrder(userId: string, sourceObjid: string): Promise<{ newObjid: string; newContractNo: string }> {
  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    // 1. 새 영업번호 채번 — wace getNextContractNo 1:1 ({YY}C-{NNNN}, MAX+1)
    const numRes = await client.query(
      `SELECT TO_CHAR(NOW(),'YY') || 'C-' || LPAD((
         COALESCE(MAX(SUBSTRING(contract_no FROM 5)::integer), 0) + 1
       )::VARCHAR, 4, '0') AS contract_no
       FROM contract_mgmt
       WHERE contract_no LIKE TO_CHAR(NOW(),'YY') || 'C-%'`,
    );
    const newContractNo = numRes.rows[0].contract_no as string;
    const newObjid = genObjid("CM");

    // 2. CONTRACT_MGMT 복사 (wace copyContractMgmt 1:1)
    const r = await client.query(
      `INSERT INTO contract_mgmt (
         objid, category_cd, customer_objid, product, area_cd,
         customer_equip_name, customer_project_name, customer_production_no, mechanical_type,
         paid_type, receipt_date, req_del_date, contract_result,
         po_no, order_date, contract_currency, exchange_rate,
         regdate, writer, contract_no, is_direct_order,
         order_supply_price, order_vat, order_total_amount
       )
       SELECT
         $1, category_cd, customer_objid, product, area_cd,
         customer_equip_name, customer_project_name, customer_production_no, mechanical_type,
         paid_type, receipt_date, req_del_date, '',
         po_no, order_date, contract_currency, exchange_rate,
         NOW(), $2, $3, 'Y',
         order_supply_price, order_vat, order_total_amount
       FROM contract_mgmt
       WHERE objid = $4`,
      [newObjid, userId, newContractNo, sourceObjid],
    );
    if (r.rowCount === 0) {
      throw new AppError("복사할 원본 주문이 없습니다.", 404);
    }

    // 3. 원본 활성 라인 OBJID 목록 (wace getActiveItemObjIds)
    const itemsRes = await client.query(
      `SELECT objid FROM contract_item
        WHERE contract_objid=$1 AND status='ACTIVE'
        ORDER BY seq`,
      [sourceObjid],
    );

    // 4. CONTRACT_ITEM + CONTRACT_ITEM_SERIAL 복사
    for (const row of itemsRes.rows) {
      const sourceItemObjid: string = row.objid;
      const newItemObjid = genObjid("CI");

      // wace copyContractItems 1:1
      await client.query(
        `INSERT INTO contract_item (
           objid, contract_objid, seq, part_objid, part_no, part_name,
           quantity, due_date, customer_request, return_reason,
           regdate, writer, status,
           order_quantity, order_unit_price, order_supply_price, order_vat, order_total_amount
         )
         SELECT
           $1, $2, seq, part_objid, part_no, part_name,
           quantity, due_date, customer_request, return_reason,
           NOW(), $3, 'ACTIVE',
           order_quantity, order_unit_price, order_supply_price, order_vat, order_total_amount
         FROM contract_item
         WHERE objid=$4 AND status='ACTIVE'`,
        [newItemObjid, newObjid, userId, sourceItemObjid],
      );

      // wace copyContractItemSerials 1:1 — objid는 prefix + seq
      const snPrefix = `${genObjid("CIS")}_`;
      await client.query(
        `INSERT INTO contract_item_serial (
           objid, item_objid, seq, serial_no, regdate, writer, status
         )
         SELECT
           $1 || seq::varchar, $2, seq, serial_no, NOW(), $3, 'ACTIVE'
         FROM contract_item_serial
         WHERE item_objid=$4 AND status='ACTIVE'`,
        [snPrefix, newItemObjid, userId, sourceItemObjid],
      );
    }

    await client.query("COMMIT");
    logger.info("수주복사 완료", { sourceObjid, newObjid, newContractNo, lines: itemsRes.rowCount });
    return { newObjid, newContractNo };
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}

// G4/G11 — 수주 결재상신 (wace ApprovalService.getAmaranthSsoUrl + orderMgmtList.btnApproval 1:1)
//   target_type='CONTRACT_ORDER', formId='1161', compSeq='1000' (wace orderMgmtList:554~559)
//   amaranth_approval 신규/기존 매핑 분기:
//     - 없음: 신규 approKey 생성 + INSERT
//     - reject/delete/create: 새 approKey + UPDATE (재상신)
//     - inProcess/complete: 기존 approKey 재사용 (프론트에서 가드되지만 백엔드도 방어)
//   wace 1:1 — 첨부파일 원챔버 업로드는 미구현 (영업관리 첨부 흐름 별도 작업).
//   wace ApprovalService.java:1782~1909.
export async function startOrderApproval(userId: string, contractObjid: string, opts: {
  approvalTitle?: string;
  subjectStr?: string;
} = {}): Promise<{ fullUrl: string; approKey: string; status: string }> {
  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    // 1) 사용자 emp_seq 조회 (wace PersonBean.getEmpseq 동등)
    const userRes = await client.query(
      `SELECT user_id, user_name, emp_seq FROM user_info WHERE user_id=$1 LIMIT 1`,
      [userId],
    );
    const u = userRes.rows[0];
    if (!u) throw new AppError("사용자 정보를 찾을 수 없습니다.", 401);
    const empSeq: string = String(u.emp_seq ?? "").trim();
    if (!empSeq) {
      throw new AppError(`empSeq 정보가 없습니다. (userId: ${userId}) 관리자에게 문의하세요.`, 400);
    }

    // 2) contract_mgmt 헤더 + 라인 수 확인 (hasOrderData 가드는 프론트지만 백엔드도 방어)
    const cmRes = await client.query(
      `SELECT cm.objid, cm.contract_no,
              (SELECT count(*) FROM contract_item WHERE contract_objid=cm.objid AND status='ACTIVE') AS line_cnt
         FROM contract_mgmt cm
        WHERE cm.objid=$1`,
      [contractObjid],
    );
    if (cmRes.rowCount === 0) throw new AppError("주문서를 찾을 수 없습니다.", 404);
    const { contract_no, line_cnt } = cmRes.rows[0];
    if (Number(line_cnt) === 0) {
      throw new AppError("수주 품목을 먼저 등록해주세요.", 400);
    }

    const targetType = "CONTRACT_ORDER";
    const approvalTitle = opts.approvalTitle || `주문서 결재${contract_no ? " - " + contract_no : ""}`;
    const outProcessCode = process.env.AMARANTH_OUT_PROCESS_CODE_CONTRACT_ORDER || process.env.AMARANTH_OUT_PROCESS_CODE || "";
    const formId = process.env.AMARANTH_FORM_ID_CONTRACT_ORDER || "1161";
    const compSeq = process.env.AMARANTH_COMP_SEQ || "1000";

    // 3) amaranth_approval 기존 매핑 조회 (wace selectAmaranthApprovalByTarget)
    const existRes = await client.query(
      `SELECT objid, appro_key, status FROM amaranth_approval
        WHERE target_type=$1 AND target_objid=$2
        ORDER BY regdate DESC LIMIT 1`,
      [targetType, contractObjid],
    );

    let approKey: string;
    let mode: "insert" | "update_resubmit" | "update_reuse";
    let existingObjid: number | null = null;

    if (existRes.rowCount === 0) {
      approKey = "UB_" + Date.now().toString(36).toUpperCase();
      mode = "insert";
    } else {
      existingObjid = existRes.rows[0].objid;
      const existingStatus = String(existRes.rows[0].status || "");
      if (["reject", "delete", "create"].includes(existingStatus)) {
        // 재상신: 새 approKey (아마란스가 기존 approKey의 원챔버 첨부를 재사용하므로 수정 미반영 회피)
        approKey = "UB_" + Date.now().toString(36).toUpperCase();
        mode = "update_resubmit";
      } else {
        approKey = String(existRes.rows[0].appro_key || "");
        mode = "update_reuse";
      }
    }

    // 4) SSO URL 발급 (chpark amaranthApprovalClient.getSsoUrl)
    const ssoRes = await amaranth.getSsoUrl({
      empSeq,
      outProcessCode: outProcessCode || undefined,
      formId,
      approKey,
      subjectStr: opts.subjectStr || approvalTitle,
      mod: "W",
      compSeq,
      deptSeq: "",
      loginId: u.user_id,
    });
    const fullUrl: string = ssoRes?.resultData?.fullUrl || ssoRes?.fullUrl || "";
    const resultCode = String(ssoRes?.resultCode ?? ssoRes?.resultData?.resultCode ?? "");

    if (!fullUrl || (resultCode !== "0" && resultCode !== "")) {
      const msg = ssoRes?.resultMsg || ssoRes?.resultData?.resultMsg || "SSO URL 생성 실패";
      throw new AppError(`결재 연동 오류: ${msg}`, 502);
    }

    // 5) amaranth_approval INSERT/UPDATE
    if (mode === "insert") {
      const objid = Date.now();
      await client.query(
        `INSERT INTO amaranth_approval
           (objid, target_objid, target_type, appro_key, out_process_code, form_id,
            status, emp_seq, comp_seq, dept_seq, writer, sso_url, regdate)
         VALUES ($1, $2, $3, $4, $5, $6, 'create', $7, $8, '', $9, $10, NOW())`,
        [
          objid, contractObjid, targetType, approKey,
          outProcessCode || null, formId, empSeq, compSeq, userId, fullUrl,
        ],
      );
    } else {
      // 재상신: approKey/sso_url/status 초기화, 재사용: sso_url만 갱신
      const resetStatus = mode === "update_resubmit" ? "create" : null;
      await client.query(
        `UPDATE amaranth_approval
            SET appro_key=$2, sso_url=$3, writer=$4,
                status=COALESCE($5, status),
                editdate=NOW()
          WHERE objid=$1`,
        [existingObjid, approKey, fullUrl, userId, resetStatus],
      );
    }

    await client.query("COMMIT");
    logger.info("수주 결재상신", { contractObjid, contract_no, approKey, mode });
    return { fullUrl, approKey, status: "create" };
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}

export async function updateStatus(userId: string, objid: string, contractResult: string) {
  const pool = getPool();
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    await client.query(
      `UPDATE contract_mgmt SET contract_result=$2, chg_user_id=$3 WHERE objid=$1`,
      [objid, contractResult, userId],
    );

    if (PROJECT_TRIGGER_RESULTS.includes(contractResult as any)) {
      await createProjectsFromContract(client, objid, userId);
    }

    await client.query("COMMIT");
    logger.info("주문 상태 변경", { objid, contractResult });
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}
