import { Response } from "express";
import { getPool } from "../database/db";
import logger from "../utils/logger";
import { AuthenticatedRequest } from "../middleware/authMiddleware";

// 불량 상세 항목 타입
interface DefectDetailItem {
  defect_code: string;
  defect_name: string;
  qty: string;
  disposition: string;
}

// 자동 마이그레이션: batch_id 컬럼 추가 (배치/로트 추적용)
let _batchMigrationDone = false;
async function ensureBatchIdColumn() {
  if (_batchMigrationDone) return;
  try {
    const pool = getPool();
    await pool.query("ALTER TABLE work_order_process ADD COLUMN IF NOT EXISTS batch_id VARCHAR(100)");
    _batchMigrationDone = true;
  } catch { /* 이미 존재하거나 권한 문제 시 무시 */ }
}

/**
 * inventory_stock UPSERT 공통 함수
 * PC의 receivingController와 동일한 SELECT→INSERT/UPDATE 패턴 사용.
 * (inventory_stock에 UNIQUE 제약조건이 없으므로 ON CONFLICT 사용 불가)
 */
async function upsertInventoryStock(
  client: { query: (text: string, values?: any[]) => Promise<any> },
  companyCode: string,
  itemCode: string,
  warehouseCode: string,
  locationCode: string | null,
  qty: number,
  userId: string
): Promise<void> {
  const whCode = warehouseCode || null;
  const locCode = locationCode || null;

  const existing = await client.query(
    `SELECT id FROM inventory_stock
     WHERE company_code = $1 AND item_code = $2
       AND COALESCE(warehouse_code, '') = COALESCE($3, '')
       AND COALESCE(location_code, '') = COALESCE($4, '')
     LIMIT 1`,
    [companyCode, itemCode, whCode || '', locCode || '']
  );

  if (existing.rows.length > 0) {
    await client.query(
      `UPDATE inventory_stock
       SET current_qty = CAST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) + $1 AS text),
           last_in_date = NOW(),
           updated_date = NOW(),
           writer = $2
       WHERE id = $3`,
      [qty, userId, existing.rows[0].id]
    );
  } else {
    await client.query(
      `INSERT INTO inventory_stock (
        id, company_code, item_code, warehouse_code, location_code,
        current_qty, safety_qty, last_in_date,
        created_date, updated_date, writer
      ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, '0', NOW(), NOW(), NOW(), $6)`,
      [companyCode, itemCode, whCode, locCode, String(qty), userId]
    );
  }
}

/**
 * 체크리스트 복사 공통 함수
 * 분할 행/재작업 카드 생성 시 마스터의 체크리스트를 새 행에 복사한다.
 *
 * 전략: routingDetailId가 있으면 원본 템플릿에서, 없으면 마스터의 기존 결과에서 복사
 */
async function copyChecklistToSplit(
  client: { query: (text: string, values?: any[]) => Promise<any> },
  masterProcessId: string,
  newProcessId: string,
  routingDetailId: string | null,
  companyCode: string,
  userId: string
): Promise<number> {
  // A. routing_detail_id가 있으면 원본 템플릿(process_work_item + detail)에서 복사
  if (routingDetailId) {
    const result = await client.query(
      `INSERT INTO process_work_result (
        id, company_code, work_order_process_id,
        source_work_item_id, source_detail_id,
        work_phase, item_title, item_sort_order,
        detail_content, detail_type, detail_sort_order, is_required,
        inspection_code, inspection_method, unit, lower_limit, upper_limit,
        input_type, lookup_target, display_fields, duration_minutes,
        status, writer
      )
      SELECT
        gen_random_uuid()::text, pwi.company_code, $1,
        pwi.id, pwd.id,
        pwi.work_phase, pwi.title, pwi.sort_order::text,
        pwd.content, pwd.detail_type, pwd.sort_order::text, pwd.is_required,
        pwd.inspection_code, pwd.inspection_method, pwd.unit, pwd.lower_limit, pwd.upper_limit,
        pwd.input_type, pwd.lookup_target, pwd.display_fields, pwd.duration_minutes::text,
        'pending', $2
      FROM process_work_item pwi
      JOIN process_work_item_detail pwd ON pwd.work_item_id = pwi.id
        AND pwd.company_code = pwi.company_code
      WHERE pwi.routing_detail_id = $3
        AND pwi.company_code = $4
      ORDER BY pwi.sort_order, pwd.sort_order`,
      [newProcessId, userId, routingDetailId, companyCode]
    );
    const countA = result.rowCount ?? 0;
    if (countA > 0) return countA;
    // A 전략에서 0건이면 B 전략(마스터 행의 기존 결과 복사)으로 fallthrough
  }

  // B. routing_detail_id가 없거나 A 전략에서 0건이면 마스터 행의 process_work_result에서 구조만 복사 (타이머/결과값 초기화)
  const result = await client.query(
    `INSERT INTO process_work_result (
      company_code, work_order_process_id,
      source_work_item_id, source_detail_id,
      work_phase, item_title, item_sort_order,
      detail_content, detail_type, detail_sort_order, is_required,
      inspection_code, inspection_method, unit, lower_limit, upper_limit,
      input_type, lookup_target, display_fields, duration_minutes,
      status, writer
    )
    SELECT
      company_code, $1,
      source_work_item_id, source_detail_id,
      work_phase, item_title, item_sort_order,
      detail_content, detail_type, detail_sort_order, is_required,
      inspection_code, inspection_method, unit, lower_limit, upper_limit,
      input_type, lookup_target, display_fields, duration_minutes,
      'pending', $2
    FROM process_work_result
    WHERE work_order_process_id = $3
      AND company_code = $4
    ORDER BY item_sort_order, detail_sort_order`,
    [newProcessId, userId, masterProcessId, companyCode]
  );
  return result.rowCount ?? 0;
}

/**
 * 내부 헬퍼: 단일 작업지시에 대해 work_order_process + process_work_result 일괄 생성
 * createWorkProcesses와 syncWorkInstructions 양쪽에서 재사용한다.
 *
 * @returns 생성된 공정 목록 + 체크리스트 총 수. 이미 존재하면 null 반환.
 */
async function generateWorkProcessesForInstruction(
  client: { query: (text: string, values?: any[]) => Promise<any> },
  workInstructionId: string,
  routingVersionId: string,
  planQty: string | null,
  companyCode: string,
  userId: string
): Promise<{
  processes: Array<{ id: string; seq_no: string; process_name: string; checklist_count: number }>;
  total_checklists: number;
} | null> {
  // 중복 호출 방지: 이미 생성된 공정이 있는지 확인
  const existCheck = await client.query(
    `SELECT COUNT(*) as cnt FROM work_order_process
     WHERE wo_id = $1 AND company_code = $2`,
    [workInstructionId, companyCode]
  );
  if (parseInt(existCheck.rows[0].cnt, 10) > 0) {
    return null; // 이미 존재
  }

  // 1. item_routing_detail + process_mng JOIN (공정 목록 + 공정명)
  const routingDetails = await client.query(
    `SELECT rd.id, rd.seq_no, rd.process_code,
            COALESCE(pm.process_name, rd.process_code) as process_name,
            rd.is_required, rd.is_fixed_order, rd.standard_time
     FROM item_routing_detail rd
     LEFT JOIN process_mng pm ON pm.process_code = rd.process_code
       AND pm.company_code = rd.company_code
     WHERE rd.routing_version_id = $1 AND rd.company_code = $2
     ORDER BY CAST(rd.seq_no AS int) NULLS LAST`,
    [routingVersionId, companyCode]
  );

  if (routingDetails.rows.length === 0) {
    return null; // 공정 없음
  }

  const processes: Array<{
    id: string;
    seq_no: string;
    process_name: string;
    checklist_count: number;
  }> = [];
  let totalChecklists = 0;

  for (const rd of routingDetails.rows) {
    // 2. work_order_process INSERT
    const wopResult = await client.query(
      `INSERT INTO work_order_process (
        id, company_code, wo_id, seq_no, process_code, process_name,
        is_required, is_fixed_order, standard_time, plan_qty,
        status, routing_detail_id, writer
      ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
      RETURNING id`,
      [
        companyCode,
        workInstructionId,
        rd.seq_no,
        rd.process_code,
        rd.process_name,
        rd.is_required,
        rd.is_fixed_order,
        rd.standard_time,
        planQty || null,
        parseInt(rd.seq_no, 10) === 1 || rd.is_fixed_order === "Y" ? "acceptable" : "waiting",
        rd.id,
        userId,
      ]
    );
    const wopId = wopResult.rows[0].id;

    // 3. process_work_result INSERT (공통 함수로 체크리스트 복사)
    const checklistCount = await copyChecklistToSplit(
      client, wopId, wopId, rd.id, companyCode, userId
    );
    totalChecklists += checklistCount;

    processes.push({
      id: wopId,
      seq_no: rd.seq_no,
      process_name: rd.process_name,
      checklist_count: checklistCount,
    });
  }

  return { processes, total_checklists: totalChecklists };
}

/**
 * D-BE1: 작업지시 공정 일괄 생성
 * PC에서 작업지시 생성 후 호출. 1 트랜잭션으로 work_order_process + process_work_result 일괄 생성.
 */
export const createWorkProcesses = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();
  const client = await pool.connect();

  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;

    const { work_instruction_id, item_code, routing_version_id, plan_qty } =
      req.body;

    if (!work_instruction_id || !routing_version_id) {
      return res.status(400).json({
        success: false,
        message:
          "work_instruction_id와 routing_version_id는 필수입니다.",
      });
    }

    logger.info("[pop/production] create-work-processes 요청", {
      companyCode,
      userId,
      work_instruction_id,
      item_code,
      routing_version_id,
      plan_qty,
    });

    await client.query("BEGIN");

    const result = await generateWorkProcessesForInstruction(
      client, work_instruction_id, routing_version_id, plan_qty, companyCode, userId
    );

    if (!result) {
      await client.query("ROLLBACK");
      return res.status(409).json({
        success: false,
        message: "이미 공정이 생성된 작업지시이거나 라우팅에 공정이 없습니다.",
      });
    }

    await client.query("COMMIT");

    logger.info("[pop/production] create-work-processes 완료", {
      companyCode,
      work_instruction_id,
      total_processes: result.processes.length,
      total_checklists: result.total_checklists,
    });

    return res.json({
      success: true,
      data: {
        processes: result.processes,
        total_processes: result.processes.length,
        total_checklists: result.total_checklists,
      },
    });
  } catch (error: any) {
    await client.query("ROLLBACK");
    logger.error("[pop/production] create-work-processes 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "공정 생성 중 오류가 발생했습니다.",
    });
  } finally {
    client.release();
  }
};

/**
 * POP 온디맨드 Pull: 미동기화 작업지시 일괄 sync
 * routing이 있지만 work_order_process가 없는 작업지시를 찾아 공정을 자동 생성한다.
 * 각 건별 개별 try-catch로 하나 실패해도 나머지 진행.
 */
export const syncWorkInstructions = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();

  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;

    logger.info("[pop/production] sync-work-instructions 요청", {
      companyCode,
      userId,
    });

    // 미동기화 작업지시 조회: routing이 있지만 work_order_process가 없는 항목
    const unsyncedResult = await pool.query(
      `SELECT wi.id, wi.work_instruction_no, wi.routing, wi.qty
       FROM work_instruction wi
       WHERE wi.company_code = $1
         AND wi.routing IS NOT NULL
         AND NOT EXISTS (
           SELECT 1 FROM work_order_process wop
           WHERE wop.wo_id = wi.id AND wop.company_code = $1
         )`,
      [companyCode]
    );

    const unsynced = unsyncedResult.rows;

    if (unsynced.length === 0) {
      return res.json({
        success: true,
        data: { synced: 0, skipped: 0, errors: 0, details: [] },
      });
    }

    let synced = 0;
    let skipped = 0;
    let errors = 0;
    const details: Array<{
      work_instruction_id: string;
      work_instruction_no: string;
      status: "synced" | "skipped" | "error";
      process_count?: number;
      error?: string;
    }> = [];

    for (const wi of unsynced) {
      const client = await pool.connect();
      try {
        await client.query("BEGIN");

        const result = await generateWorkProcessesForInstruction(
          client, wi.id, wi.routing, wi.qty || null, companyCode, userId
        );

        if (!result) {
          await client.query("ROLLBACK");
          skipped++;
          details.push({
            work_instruction_id: wi.id,
            work_instruction_no: wi.work_instruction_no,
            status: "skipped",
          });
          continue;
        }

        await client.query("COMMIT");
        synced++;
        details.push({
          work_instruction_id: wi.id,
          work_instruction_no: wi.work_instruction_no,
          status: "synced",
          process_count: result.processes.length,
        });

        logger.info("[pop/production] sync: 공정 생성 완료", {
          work_instruction_no: wi.work_instruction_no,
          process_count: result.processes.length,
        });
      } catch (err: any) {
        await client.query("ROLLBACK");
        errors++;
        details.push({
          work_instruction_id: wi.id,
          work_instruction_no: wi.work_instruction_no,
          status: "error",
          error: err.message || "알 수 없는 오류",
        });
        logger.error("[pop/production] sync: 개별 오류", {
          work_instruction_no: wi.work_instruction_no,
          error: err.message,
        });
      } finally {
        client.release();
      }
    }

    logger.info("[pop/production] sync-work-instructions 완료", {
      companyCode,
      synced,
      skipped,
      errors,
    });

    return res.json({
      success: true,
      data: { synced, skipped, errors, details },
    });
  } catch (error: any) {
    logger.error("[pop/production] sync-work-instructions 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "작업지시 동기화 중 오류가 발생했습니다.",
    });
  }
};

/**
 * D-BE2: 타이머 API (시작/일시정지/재시작)
 */
export const controlTimer = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();

  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;

    const { work_order_process_id, action } = req.body;

    if (!work_order_process_id || !action) {
      return res.status(400).json({
        success: false,
        message: "work_order_process_id와 action은 필수입니다.",
      });
    }

    if (!["start", "pause", "resume", "complete"].includes(action)) {
      return res.status(400).json({
        success: false,
        message:
          "action은 start, pause, resume, complete 중 하나여야 합니다.",
      });
    }

    logger.info("[pop/production] timer 요청", {
      companyCode,
      userId,
      work_order_process_id,
      action,
    });

    let result;

    switch (action) {
      case "start":
        // 최초 1회만 설정, 이미 있으면 무시
        result = await pool.query(
          `UPDATE work_order_process
           SET started_at = CASE WHEN started_at IS NULL THEN NOW()::text ELSE started_at END,
               status = CASE WHEN status = 'waiting' THEN 'in_progress' ELSE status END,
               updated_date = NOW()
           WHERE id = $1 AND company_code = $2
           RETURNING id, started_at, status`,
          [work_order_process_id, companyCode]
        );
        break;

      case "pause":
        result = await pool.query(
          `UPDATE work_order_process
           SET paused_at = NOW()::text,
               updated_date = NOW()
           WHERE id = $1 AND company_code = $2 AND paused_at IS NULL
           RETURNING id, paused_at`,
          [work_order_process_id, companyCode]
        );
        break;

      case "resume":
        // 일시정지 시간 누적 후 paused_at 초기화
        result = await pool.query(
          `UPDATE work_order_process
           SET total_paused_time = (
             COALESCE(total_paused_time::int, 0)
             + EXTRACT(EPOCH FROM NOW() - paused_at::timestamp)::int
           )::text,
           paused_at = NULL,
           updated_date = NOW()
           WHERE id = $1 AND company_code = $2 AND paused_at IS NOT NULL
           RETURNING id, total_paused_time`,
          [work_order_process_id, companyCode]
        );
        break;

      case "complete": {
        const { good_qty, defect_qty } = req.body;

        const groupSumResult = await pool.query(
          `SELECT COALESCE(SUM(
            CASE WHEN group_started_at IS NOT NULL AND group_completed_at IS NOT NULL THEN
              EXTRACT(EPOCH FROM group_completed_at::timestamp - group_started_at::timestamp)::int
              - COALESCE(group_total_paused_time::int, 0)
            ELSE 0 END
          ), 0)::text AS total_work_seconds
          FROM process_work_result
          WHERE work_order_process_id = $1 AND company_code = $2`,
          [work_order_process_id, companyCode]
        );
        const calculatedWorkTime = groupSumResult.rows[0]?.total_work_seconds || "0";

        result = await pool.query(
          `UPDATE work_order_process
           SET status = 'completed',
               completed_at = NOW()::text,
               completed_by = $3,
               actual_work_time = $4,
               good_qty = COALESCE($5, good_qty),
               defect_qty = COALESCE($6, defect_qty),
               paused_at = NULL,
               updated_date = NOW()
           WHERE id = $1 AND company_code = $2
             AND status != 'completed'
           RETURNING id, status, completed_at, completed_by, actual_work_time, good_qty, defect_qty`,
          [
            work_order_process_id,
            companyCode,
            userId,
            calculatedWorkTime,
            good_qty || null,
            defect_qty || null,
          ]
        );
        break;
      }
    }

    if (!result || result.rowCount === 0) {
      return res.status(404).json({
        success: false,
        message: "대상 공정을 찾을 수 없거나 현재 상태에서 수행할 수 없습니다.",
      });
    }

    logger.info("[pop/production] timer 완료", {
      action,
      work_order_process_id,
      result: result.rows[0],
    });

    return res.json({
      success: true,
      data: result.rows[0],
    });
  } catch (error: any) {
    logger.error("[pop/production] timer 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "타이머 처리 중 오류가 발생했습니다.",
    });
  }
};

/**
 * 그룹(작업항목)별 타이머 제어
 * 좌측 사이드바의 각 작업 그룹마다 독립적인 시작/정지/재개/완료 타이머
 */
export const controlGroupTimer = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();

  try {
    const companyCode = req.user!.companyCode;
    const { work_order_process_id, source_work_item_id, action } = req.body;

    if (!work_order_process_id || !source_work_item_id || !action) {
      return res.status(400).json({
        success: false,
        message:
          "work_order_process_id, source_work_item_id, action은 필수입니다.",
      });
    }

    if (!["start", "pause", "resume", "complete"].includes(action)) {
      return res.status(400).json({
        success: false,
        message:
          "action은 start, pause, resume, complete 중 하나여야 합니다.",
      });
    }

    logger.info("[pop/production] group-timer 요청", {
      companyCode,
      work_order_process_id,
      source_work_item_id,
      action,
    });

    const whereClause = `work_order_process_id = $1 AND source_work_item_id = $2 AND company_code = $3`;
    const baseParams = [work_order_process_id, source_work_item_id, companyCode];

    let result;

    switch (action) {
      case "start":
        result = await pool.query(
          `UPDATE process_work_result
           SET group_started_at = CASE WHEN group_started_at IS NULL THEN NOW()::text ELSE group_started_at END,
               updated_date = NOW()
           WHERE ${whereClause}
           RETURNING id, group_started_at`,
          baseParams
        );
        await pool.query(
          `UPDATE work_order_process
           SET started_at = NOW()::text, updated_date = NOW()
           WHERE id = $1 AND company_code = $2 AND started_at IS NULL`,
          [work_order_process_id, companyCode]
        );
        break;

      case "pause":
        result = await pool.query(
          `UPDATE process_work_result
           SET group_paused_at = NOW()::text,
               updated_date = NOW()
           WHERE ${whereClause} AND group_paused_at IS NULL
           RETURNING id, group_paused_at`,
          baseParams
        );
        break;

      case "resume":
        result = await pool.query(
          `UPDATE process_work_result
           SET group_total_paused_time = (
             COALESCE(group_total_paused_time::int, 0)
             + EXTRACT(EPOCH FROM NOW() - group_paused_at::timestamp)::int
           )::text,
           group_paused_at = NULL,
           updated_date = NOW()
           WHERE ${whereClause} AND group_paused_at IS NOT NULL
           RETURNING id, group_total_paused_time`,
          baseParams
        );
        break;

      case "complete": {
        result = await pool.query(
          `UPDATE process_work_result
           SET group_completed_at = NOW()::text,
               group_total_paused_time = CASE
                 WHEN group_paused_at IS NOT NULL THEN (
                   COALESCE(group_total_paused_time::int, 0)
                   + EXTRACT(EPOCH FROM NOW() - group_paused_at::timestamp)::int
                 )::text
                 ELSE group_total_paused_time
               END,
               group_paused_at = NULL,
               updated_date = NOW()
           WHERE ${whereClause}
           RETURNING id, group_started_at, group_completed_at, group_total_paused_time`,
          baseParams
        );
        break;
      }
    }

    if (!result || result.rowCount === 0) {
      return res.status(404).json({
        success: false,
        message: "대상 그룹을 찾을 수 없거나 현재 상태에서 수행할 수 없습니다.",
      });
    }

    logger.info("[pop/production] group-timer 완료", {
      action,
      source_work_item_id,
      affectedRows: result.rowCount,
    });

    return res.json({
      success: true,
      data: result.rows[0],
      affectedRows: result.rowCount,
    });
  } catch (error: any) {
    logger.error("[pop/production] group-timer 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "그룹 타이머 처리 중 오류가 발생했습니다.",
    });
  }
};

/**
 * 불량 유형 목록 조회 (defect_standard_mng)
 */
export const getDefectTypes = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();

  try {
    const companyCode = req.user!.companyCode;

    let query: string;
    let params: unknown[];

    if (companyCode === "*") {
      query = `
        SELECT id, defect_code, defect_name, defect_type, severity, company_code
        FROM defect_standard_mng
        WHERE is_active = 'Y'
        ORDER BY defect_code`;
      params = [];
    } else {
      query = `
        SELECT id, defect_code, defect_name, defect_type, severity, company_code
        FROM defect_standard_mng
        WHERE is_active = 'Y' AND company_code = $1
        ORDER BY defect_code`;
      params = [companyCode];
    }

    const result = await pool.query(query, params);

    logger.info("[pop/production] defect-types 조회", {
      companyCode,
      count: result.rowCount,
    });

    return res.json({
      success: true,
      data: result.rows,
    });
  } catch (error: any) {
    logger.error("[pop/production] defect-types 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "불량 유형 조회 중 오류가 발생했습니다.",
    });
  }
};

/**
 * 실적 저장 (누적 방식)
 * 이번 차수 생산수량을 기존 누적치에 더한다.
 * result_status는 'draft' 유지 (확정 전까지 계속 추가 등록 가능)
 */
export const saveResult = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();

  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;

    const {
      work_order_process_id,
      production_qty,
      good_qty,
      defect_qty,
      defect_detail,
      result_note,
    } = req.body;

    if (!work_order_process_id) {
      return res.status(400).json({
        success: false,
        message: "work_order_process_id는 필수입니다.",
      });
    }

    if (!production_qty || parseInt(production_qty, 10) <= 0) {
      return res.status(400).json({
        success: false,
        message: "생산수량을 입력해주세요.",
      });
    }

    const statusCheck = await pool.query(
      `SELECT wop.status, wop.result_status, wop.total_production_qty, wop.good_qty,
              wop.defect_qty, wop.concession_qty, wop.defect_detail,
              wop.input_qty, wop.parent_process_id, wop.wo_id, wop.seq_no
       FROM work_order_process wop
       WHERE wop.id = $1 AND wop.company_code = $2`,
      [work_order_process_id, companyCode]
    );

    if (statusCheck.rowCount === 0) {
      return res.status(404).json({
        success: false,
        message: "공정을 찾을 수 없습니다.",
      });
    }

    const prev = statusCheck.rows[0];

    // 마스터 행에 직접 실적 등록 방지 (분할 행이 존재하는 경우)
    if (!prev.parent_process_id) {
      const splitCheck = await pool.query(
        `SELECT COUNT(*) as cnt FROM work_order_process
         WHERE parent_process_id = $1 AND company_code = $2`,
        [work_order_process_id, companyCode]
      );
      if (parseInt(splitCheck.rows[0].cnt, 10) > 0) {
        return res.status(400).json({
          success: false,
          message: "원본 공정에는 직접 실적을 등록할 수 없습니다. 분할된 접수 카드에서 등록해주세요.",
        });
      }
    }

    if (prev.result_status === "confirmed") {
      return res.status(403).json({
        success: false,
        message: "이미 확정된 실적입니다. 추가 등록이 불가능합니다.",
      });
    }

    // 초과 생산 경고 (차단하지 않음 - 현장 유연성)
    const prevTotal = parseInt(prev.total_production_qty, 10) || 0;
    const acceptedQty = parseInt(prev.input_qty, 10) || 0;
    const requestedQty = parseInt(production_qty, 10) || 0;
    if (acceptedQty > 0 && (prevTotal + requestedQty) > acceptedQty) {
      logger.warn("[pop/production] 초과 생산 감지", {
        work_order_process_id,
        prevTotal, requestedQty, acceptedQty,
        overAmount: (prevTotal + requestedQty) - acceptedQty,
      });
    }

    // 서버 측 양품/불량/특채 계산 (클라이언트 good_qty는 참고만)
    const addProduction = parseInt(production_qty, 10) || 0;
    let addDefect = 0;
    let addConcession = 0;

    let defectDetailStr: string | null = null;
    if (defect_detail && Array.isArray(defect_detail)) {
      const validated = defect_detail.map((item: DefectDetailItem) => ({
        defect_code: item.defect_code || "",
        defect_name: item.defect_name || "",
        qty: item.qty || "0",
        disposition: item.disposition || "scrap",
      }));
      defectDetailStr = JSON.stringify(validated);

      for (const item of validated) {
        const itemQty = parseInt(item.qty, 10) || 0;
        addDefect += itemQty;
        if (item.disposition === "accept") {
          addConcession += itemQty;
        }
      }
    } else {
      addDefect = parseInt(defect_qty, 10) || 0;
    }
    const addGood = addProduction - addDefect;

    const newTotal = (parseInt(prev.total_production_qty, 10) || 0) + addProduction;
    const newGood = (parseInt(prev.good_qty, 10) || 0) + addGood;
    const newDefect = (parseInt(prev.defect_qty, 10) || 0) + addDefect;
    const newConcession = (parseInt(prev.concession_qty, 10) || 0) + addConcession;

    // 기존 defect_detail에 이번 차수 상세를 병합
    let mergedDefectDetail: string | null = null;
    if (defectDetailStr) {
      let existingEntries: DefectDetailItem[] = [];
      try {
        existingEntries = prev.defect_detail ? JSON.parse(prev.defect_detail) : [];
      } catch { /* 파싱 실패 시 빈 배열 */ }
      const newEntries: DefectDetailItem[] = JSON.parse(defectDetailStr);
      const merged = [...existingEntries];
      for (const ne of newEntries) {
        const existing = merged.find(
          (e) => e.defect_code === ne.defect_code && e.disposition === ne.disposition
        );
        if (existing) {
          existing.qty = String(
            (parseInt(existing.qty, 10) || 0) + (parseInt(ne.qty, 10) || 0)
          );
        } else {
          merged.push(ne);
        }
      }
      mergedDefectDetail = JSON.stringify(merged);
    }

    const result = await pool.query(
      `UPDATE work_order_process
       SET total_production_qty = $3,
           good_qty = $4,
           defect_qty = $5,
           concession_qty = $9,
           defect_detail = COALESCE($6, defect_detail),
           result_note = COALESCE($7, result_note),
           result_status = 'draft',
           status = CASE WHEN status IN ('acceptable', 'waiting') THEN 'in_progress' ELSE status END,
           writer = $8,
           updated_date = NOW()
       WHERE id = $1 AND company_code = $2
       RETURNING id, total_production_qty, good_qty, defect_qty, concession_qty, defect_detail, result_note, result_status, status`,
      [
        work_order_process_id,
        companyCode,
        String(newTotal),
        String(newGood),
        String(newDefect),
        mergedDefectDetail,
        result_note || null,
        userId,
        String(newConcession),
      ]
    );

    if (result.rowCount === 0) {
      return res.status(404).json({
        success: false,
        message: "공정을 찾을 수 없거나 권한이 없습니다.",
      });
    }

    // === BUG-2 FIX: SPLIT 실적 저장 후 master 행에 합산 ===
    if (prev.parent_process_id) {
      await pool.query(
        `UPDATE work_order_process
         SET good_qty = sub.sum_good,
             defect_qty = sub.sum_defect,
             total_production_qty = sub.sum_total,
             concession_qty = sub.sum_concession,
             updated_date = NOW()
         FROM (
           SELECT
             COALESCE(SUM(CAST(NULLIF(good_qty, '') AS numeric)), 0)::text AS sum_good,
             COALESCE(SUM(CAST(NULLIF(defect_qty, '') AS numeric)), 0)::text AS sum_defect,
             COALESCE(SUM(CAST(NULLIF(total_production_qty, '') AS numeric)), 0)::text AS sum_total,
             COALESCE(SUM(CAST(NULLIF(concession_qty, '') AS numeric)), 0)::text AS sum_concession
           FROM work_order_process
           WHERE parent_process_id = $1 AND company_code = $2
         ) sub
         WHERE id = $1 AND company_code = $2`,
        [prev.parent_process_id, companyCode]
      );
      logger.info("[pop/production] master 합산 업데이트", {
        masterId: prev.parent_process_id,
        splitId: work_order_process_id,
      });
    }

    // 현재 분할 행의 공정 정보 조회
    const currentSeq = await pool.query(
      `SELECT wop.seq_no, wop.wo_id, wop.input_qty as current_input_qty,
              wop.parent_process_id, wop.process_code, wop.process_name,
              wop.is_required, wop.is_fixed_order, wop.standard_time,
              wop.equipment_code, wop.routing_detail_id,
              wi.qty as instruction_qty
       FROM work_order_process wop
       JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code
       WHERE wop.id = $1 AND wop.company_code = $2`,
      [work_order_process_id, companyCode]
    );

    // 재작업 카드 자동 생성 (disposition = 'rework' 항목이 있을 때)
    if (currentSeq.rowCount > 0 && defect_detail && Array.isArray(defect_detail)) {
      let totalReworkQty = 0;
      let targetProcessCode: string | null = null;
      for (const item of defect_detail) {
        if (item.disposition === "rework") {
          totalReworkQty += parseInt(item.qty, 10) || 0;
          if (item.target_process_code) targetProcessCode = item.target_process_code;
        }
      }
      if (totalReworkQty > 0) {
        const proc = currentSeq.rows[0];
        const masterId = proc.parent_process_id || work_order_process_id;

        // 재작업 대상 공정 결정
        let reworkSeqNo = proc.seq_no;
        let reworkProcessCode = proc.process_code;
        let reworkProcessName = proc.process_name;
        let reworkRoutingDetailId = proc.routing_detail_id;
        let reworkMasterId = masterId;

        // target_process_code가 지정되면 해당 공정 정보를 조회
        if (targetProcessCode) {
          const targetProc = await pool.query(
            `SELECT id, seq_no, process_code, process_name, routing_detail_id
             FROM work_order_process
             WHERE wo_id = $1 AND process_code = $2 AND company_code = $3
               AND parent_process_id IS NULL
             LIMIT 1`,
            [proc.wo_id, targetProcessCode, companyCode]
          );
          if (targetProc.rowCount > 0) {
            const tp = targetProc.rows[0];
            reworkSeqNo = tp.seq_no;
            reworkProcessCode = tp.process_code;
            reworkProcessName = tp.process_name;
            reworkRoutingDetailId = tp.routing_detail_id;
            reworkMasterId = tp.id; // 지정 공정의 마스터 ID
          }
        }

        const reworkInsert = await pool.query(
          `INSERT INTO work_order_process (
            id, wo_id, seq_no, process_code, process_name, is_required, is_fixed_order,
            standard_time, equipment_code, routing_detail_id,
            status, input_qty, good_qty, defect_qty, concession_qty, total_production_qty,
            result_status, is_rework, rework_source_id,
            parent_process_id, company_code, writer
          ) VALUES (
            gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9,
            'acceptable', $10, '0', '0', '0', '0',
            'draft', 'Y', $11,
            $12, $13, $14
          ) RETURNING id`,
          [
            proc.wo_id, reworkSeqNo, reworkProcessCode, reworkProcessName,
            proc.is_required, proc.is_fixed_order, proc.standard_time,
            proc.equipment_code, reworkRoutingDetailId,
            String(totalReworkQty), work_order_process_id,
            reworkMasterId, companyCode, userId,
          ]
        );
        // 재작업 카드에 체크리스트 복사
        const reworkId = reworkInsert.rows[0]?.id;
        if (reworkId) {
          const reworkChecklistCount = await copyChecklistToSplit(
            pool, reworkMasterId, reworkId, reworkRoutingDetailId, companyCode, userId
          );
          logger.info("[pop/production] 재작업 카드 자동 생성", {
            reworkId,
            sourceId: work_order_process_id,
            reworkQty: totalReworkQty,
            targetProcess: targetProcessCode || "(같은 공정)",
            reworkSeqNo,
            checklistCount: reworkChecklistCount,
          });
        }
      }
    }

    // 개별 분할 행 자동완료 (다음 공정 활성화보다 먼저 실행)
    if (currentSeq.rowCount > 0) {
      const { seq_no: csSeq, wo_id: csWoId, current_input_qty: csInputQty, instruction_qty: csInstrQty, parent_process_id: csParentId } = currentSeq.rows[0];
      const csMyInput = parseInt(csInputQty, 10) || 0;

      if (newTotal >= csMyInput && csMyInput > 0) {
        await pool.query(
          `UPDATE work_order_process SET status = 'completed', result_status = 'confirmed',
           completed_at = NOW()::text, completed_by = $3, updated_date = NOW()
           WHERE id = $1 AND company_code = $2 AND status != 'completed'`,
          [work_order_process_id, companyCode, userId]
        );

        // 같은 seq의 모든 분할 행 완료 체크 → 마스터도 completed
        const csSeqNum = parseInt(csSeq, 10);
        let csPrevGood = parseInt(csInstrQty, 10) || 0;
        if (csSeqNum > 1) {
          const prev = await pool.query(
            `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as tg
             FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
               AND parent_process_id IS NOT NULL`,
            [csWoId, String(csSeqNum - 1), companyCode]
          );
          if (prev.rowCount > 0) csPrevGood = parseInt(prev.rows[0].tg, 10) || 0;
        }
        const sibCheck = await pool.query(
          `SELECT COALESCE(SUM(input_qty::int), 0) as ti, COUNT(*) FILTER (WHERE status != 'completed') as ic
           FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`,
          [csWoId, csSeq, companyCode]
        );
        const csTotalInput = parseInt(sibCheck.rows[0].ti, 10) || 0;
        const csIncomplete = parseInt(sibCheck.rows[0].ic, 10) || 0;
        if (csIncomplete === 0 && csPrevGood - csTotalInput <= 0 && csParentId) {
          await pool.query(
            `UPDATE work_order_process SET status = 'completed', result_status = 'confirmed',
             completed_at = NOW()::text, completed_by = $3, updated_date = NOW()
             WHERE id = $1 AND company_code = $2 AND status != 'completed'`,
            [csParentId, companyCode, userId]
          );
        }
      }

      await checkAndCompleteWorkInstruction(pool, csWoId, companyCode, userId);
    }

    // 다음 공정 활성화 (다중공정 대응)
    // is_fixed_order='Y' 그룹이면 그룹 전체 완료 후 다음 활성화
    if (addGood > 0 && currentSeq.rowCount > 0) {
      const { seq_no, wo_id, is_fixed_order } = currentSeq.rows[0];
      const seqNum = parseInt(seq_no, 10);

      let shouldActivateNext = true;

      if (is_fixed_order === "Y") {
        // 같은 seq_no에서 is_fixed_order='Y'인 병렬 공정이 모두 완료되었는지 확인
        // (병렬 그룹 = 같은 seq_no를 공유하는 공정들)
        const groupCheck = await pool.query(
          `SELECT id, seq_no, status,
                  COALESCE(good_qty::int, 0) + COALESCE(concession_qty::int, 0) as total_good
           FROM work_order_process
           WHERE wo_id = $1 AND company_code = $2
             AND parent_process_id IS NULL
             AND seq_no = $3
           ORDER BY CAST(seq_no AS int)`,
          [wo_id, companyCode, seq_no]
        );

        // 같은 seq의 미완료 공정 확인 (병렬 그룹 내)
        const incomplete = groupCheck.rows.filter((r: Record<string, unknown>) =>
          String(r.status) !== "completed" && parseInt(String(r.total_good), 10) <= 0
        );
        shouldActivateNext = incomplete.length === 0;

        if (!shouldActivateNext) {
          logger.info("[pop/production] 병렬 그룹 미완료 — 다음 공정 대기", {
            groupSize: groupCheck.rows.length,
            incomplete: incomplete.length,
          });
        }
      }

      if (shouldActivateNext) {
        // 다음 seq 활성화 (completed도 재활성화 — 새 양품이 들어오면 추가 접수 가능)
        const nextSeq = String(seqNum + 1);
        const nextUpdate = await pool.query(
          `UPDATE work_order_process
           SET status = 'acceptable',
               updated_date = NOW()
           WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
             AND parent_process_id IS NULL
           RETURNING id, process_name, status`,
          [wo_id, nextSeq, companyCode]
        );
        if (nextUpdate.rowCount > 0) {
          logger.info("[pop/production] 다음 공정 상태 전환", {
            nextProcess: nextUpdate.rows[0],
          });
        }
      }
    }

    // (분할행 완료 + 마스터 캐스케이드는 위에서 이미 처리됨)

    logger.info("[pop/production] save-result 완료 (누적)", {
      companyCode,
      work_order_process_id,
      added: { production_qty: addProduction, good_qty: addGood, defect_qty: addDefect },
      accumulated: { total: newTotal, good: newGood, defect: newDefect },
    });

    // 자동 완료 후 최신 데이터 반환 (status가 변경되었을 수 있음)
    const latestData = await pool.query(
      `SELECT id, total_production_qty, good_qty, defect_qty, concession_qty, defect_detail, result_note, result_status, status, input_qty
       FROM work_order_process WHERE id = $1 AND company_code = $2`,
      [work_order_process_id, companyCode]
    );

    // 리워크 정보도 응답에 포함 (프론트에서 다음 공정 접수 시 전달 가능)
    const responseData = latestData.rows[0] || result.rows[0];
    if (responseData) {
      const reworkInfo = await pool.query(
        `SELECT is_rework, rework_source_id FROM work_order_process WHERE id = $1`,
        [work_order_process_id]
      );
      if (reworkInfo.rows[0]?.rework_source_id) {
        responseData.rework_source_id = reworkInfo.rows[0].rework_source_id;
        responseData.is_rework = reworkInfo.rows[0].is_rework;
      }
    }

    return res.json({
      success: true,
      data: responseData,
    });
  } catch (error: any) {
    logger.error("[pop/production] save-result 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "실적 저장 중 오류가 발생했습니다.",
    });
  }
};

/**
 * 작업지시(work_instruction) 전체 완료 판정
 * 마지막 공정의 모든 행이 completed이면 작업지시도 완료 처리
 */
const checkAndCompleteWorkInstruction = async (
  pool: any,
  woId: string,
  companyCode: string,
  userId: string
) => {
  const maxSeqResult = await pool.query(
    `SELECT MAX(seq_no::int) as max_seq
     FROM work_order_process
     WHERE wo_id = $1 AND company_code = $2`,
    [woId, companyCode]
  );

  if (maxSeqResult.rowCount === 0 || !maxSeqResult.rows[0].max_seq) return;

  const maxSeq = String(maxSeqResult.rows[0].max_seq);

  const incompleteCheck = await pool.query(
    `SELECT COUNT(*) as cnt
     FROM work_order_process
     WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
       AND status != 'completed'`,
    [woId, maxSeq, companyCode]
  );

  if (parseInt(incompleteCheck.rows[0].cnt, 10) > 0) return;

  const totalGoodResult = await pool.query(
    `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good
     FROM work_order_process
     WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
       AND parent_process_id IS NOT NULL`,
    [woId, maxSeq, companyCode]
  );

  const completedQty = totalGoodResult.rows[0].total_good;

  const updateResult = await pool.query(
    `UPDATE work_instruction
     SET status = 'completed',
         progress_status = 'completed',
         completed_qty = $3,
         writer = $4,
         updated_date = NOW()
     WHERE id = $1 AND company_code = $2
       AND status != 'completed'
     RETURNING id, item_id`,
    [woId, companyCode, String(completedQty), userId]
  );

  logger.info("[pop/production] 작업지시 전체 완료", {
    woId, completedQty, companyCode,
  });

  // 생산완료→재고 입고: 마지막 공정의 target_warehouse_id가 설정된 경우 inventory_stock UPSERT
  if (updateResult.rowCount > 0 && completedQty > 0) {
    try {
      const itemId = updateResult.rows[0].item_id;

      // item_info에서 item_number 조회
      const itemResult = await pool.query(
        `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`,
        [itemId, companyCode]
      );
      if (itemResult.rowCount === 0) {
        logger.warn("[pop/production] 재고입고 건너뜀: item_info 없음", { itemId, companyCode });
        return;
      }
      const itemCode = itemResult.rows[0].item_number;

      // 마지막 공정의 창고 설정 조회 (마스터 행에서)
      const warehouseResult = await pool.query(
        `SELECT target_warehouse_id, target_location_code
         FROM work_order_process
         WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
           AND parent_process_id IS NULL
         LIMIT 1`,
        [woId, maxSeq, companyCode]
      );

      if (warehouseResult.rowCount === 0 || !warehouseResult.rows[0].target_warehouse_id) {
        logger.info("[pop/production] 재고입고 건너뜀: 목표창고 미설정", { woId });
        return;
      }

      const warehouseCode = warehouseResult.rows[0].target_warehouse_id;
      const locationCode = warehouseResult.rows[0].target_location_code || warehouseCode;

      // inventory_stock UPSERT (PC receivingController와 동일한 SELECT→INSERT/UPDATE 패턴)
      await upsertInventoryStock(pool, companyCode, itemCode, warehouseCode, locationCode, completedQty, userId);

      logger.info("[pop/production] 생산완료→재고 입고 완료", {
        woId, itemCode, warehouseCode, locationCode, qty: completedQty, companyCode,
      });
    } catch (inventoryError: any) {
      // 재고 입고 실패해도 공정 완료는 유지 (재고는 보조 기능)
      logger.error("[pop/production] 재고입고 오류 (공정 완료는 유지):", inventoryError);
    }
  }
};

/**
 * 실적 확정은 더 이상 단일 확정이 아님.
 * 마지막 등록 확인 용도로 유지. 실적은 save-result에서 차수별로 쌓임.
 */
export const confirmResult = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();

  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;

    const { work_order_process_id } = req.body;

    if (!work_order_process_id) {
      return res.status(400).json({
        success: false,
        message: "work_order_process_id는 필수입니다.",
      });
    }

    const statusCheck = await pool.query(
      `SELECT status, result_status, total_production_qty FROM work_order_process
       WHERE id = $1 AND company_code = $2`,
      [work_order_process_id, companyCode]
    );

    if (statusCheck.rowCount === 0) {
      return res.status(404).json({
        success: false,
        message: "공정을 찾을 수 없습니다.",
      });
    }

    const currentProcess = statusCheck.rows[0];

    if (!currentProcess.total_production_qty ||
        parseInt(currentProcess.total_production_qty, 10) <= 0) {
      return res.status(400).json({
        success: false,
        message: "등록된 실적이 없습니다. 실적을 먼저 등록해주세요.",
      });
    }

    // 수동 확정: 무조건 completed 처리 (수동 완료 용도)
    const result = await pool.query(
      `UPDATE work_order_process
       SET result_status = 'confirmed',
           status = 'completed',
           completed_at = NOW()::text,
           completed_by = $3,
           writer = $3,
           updated_date = NOW()
       WHERE id = $1 AND company_code = $2
       RETURNING id, status, result_status, total_production_qty, good_qty, defect_qty`,
      [work_order_process_id, companyCode, userId]
    );

    if (result.rowCount === 0) {
      return res.status(404).json({
        success: false,
        message: "공정을 찾을 수 없습니다.",
      });
    }

    // 공정 정보 조회 (다음 공정 활성화 + 마스터 캐스케이드용)
    const seqCheck = await pool.query(
      `SELECT wop.seq_no, wop.wo_id, wop.parent_process_id,
              wi.qty as instruction_qty
       FROM work_order_process wop
       JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code
       WHERE wop.id = $1 AND wop.company_code = $2`,
      [work_order_process_id, companyCode]
    );

    if (seqCheck.rowCount > 0) {
      const { seq_no, wo_id, parent_process_id, instruction_qty } = seqCheck.rows[0];
      const seqNum = parseInt(seq_no, 10);
      const instrQty = parseInt(instruction_qty, 10) || 0;

      // 다음 공정 활성화 (양품이 있으면)
      const goodQty = parseInt(result.rows[0].good_qty, 10) || 0;
      if (goodQty > 0) {
        const nextSeq = String(seqNum + 1);
        await pool.query(
          `UPDATE work_order_process
           SET status = 'acceptable',
               updated_date = NOW()
           WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
             AND parent_process_id IS NULL`,
          [wo_id, nextSeq, companyCode]
        );
      }

      // === BUG-2 FIX: confirmResult에서도 master 합산 ===
      if (parent_process_id) {
        await pool.query(
          `UPDATE work_order_process
           SET good_qty = sub.sum_good,
               defect_qty = sub.sum_defect,
               total_production_qty = sub.sum_total,
               concession_qty = sub.sum_concession,
               updated_date = NOW()
           FROM (
             SELECT
               COALESCE(SUM(CAST(NULLIF(good_qty, '') AS numeric)), 0)::text AS sum_good,
               COALESCE(SUM(CAST(NULLIF(defect_qty, '') AS numeric)), 0)::text AS sum_defect,
               COALESCE(SUM(CAST(NULLIF(total_production_qty, '') AS numeric)), 0)::text AS sum_total,
               COALESCE(SUM(CAST(NULLIF(concession_qty, '') AS numeric)), 0)::text AS sum_concession
             FROM work_order_process
             WHERE parent_process_id = $1 AND company_code = $2
           ) sub
           WHERE id = $1 AND company_code = $2`,
          [parent_process_id, companyCode]
        );
      }

      // 마스터 자동완료 캐스케이드 (분할 행인 경우)
      if (parent_process_id) {
        let prevGoodQty = instrQty;
        if (seqNum > 1) {
          const prevSeq = String(seqNum - 1);
          const prevProcess = await pool.query(
            `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good
             FROM work_order_process
             WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
               AND parent_process_id IS NOT NULL`,
            [wo_id, prevSeq, companyCode]
          );
          if (prevProcess.rowCount > 0) {
            prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0;
          }
        }

        const siblingCheck = await pool.query(
          `SELECT
             COALESCE(SUM(input_qty::int), 0) as total_input,
             COUNT(*) FILTER (WHERE status != 'completed') as incomplete_count
           FROM work_order_process
           WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
             AND parent_process_id IS NOT NULL`,
          [wo_id, seq_no, companyCode]
        );

        const totalInput = parseInt(siblingCheck.rows[0].total_input, 10) || 0;
        const incompleteCount = parseInt(siblingCheck.rows[0].incomplete_count, 10) || 0;
        const remainingAcceptable = prevGoodQty - totalInput;

        if (incompleteCount === 0 && remainingAcceptable <= 0) {
          await pool.query(
            `UPDATE work_order_process
             SET status = 'completed',
                 result_status = 'confirmed',
                 completed_at = NOW()::text,
                 completed_by = $3,
                 updated_date = NOW()
             WHERE id = $1 AND company_code = $2
               AND status != 'completed'`,
            [parent_process_id, companyCode, userId]
          );
          logger.info("[pop/production] confirmResult: 마스터 자동 완료", {
            masterId: parent_process_id, totalInput, prevGoodQty,
          });
        }
      }

      // 작업지시 전체 완료 판정
      await checkAndCompleteWorkInstruction(pool, wo_id, companyCode, userId);
    }

    logger.info("[pop/production] confirm-result 완료", {
      companyCode,
      work_order_process_id,
      userId,
      finalStatus: result.rows[0].status,
    });

    return res.json({
      success: true,
      data: result.rows[0],
    });
  } catch (error: any) {
    logger.error("[pop/production] confirm-result 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "실적 확정 중 오류가 발생했습니다.",
    });
  }
};

/**
 * 실적 이력 조회 (work_order_process_log에서 차수별 추출)
 * total_production_qty 변경 이력 = 각 차수의 등록 기록
 */
export const getResultHistory = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();

  try {
    const companyCode = req.user!.companyCode;
    const rawWopId = req.query.work_order_process_id;
    const work_order_process_id = Array.isArray(rawWopId) ? rawWopId[0] : rawWopId;

    if (!work_order_process_id) {
      return res.status(400).json({
        success: false,
        message: "work_order_process_id는 필수입니다.",
      });
    }

    // 소유권 확인
    const ownerCheck = await pool.query(
      `SELECT id FROM work_order_process WHERE id = $1 AND company_code = $2`,
      [work_order_process_id, companyCode]
    );
    if (ownerCheck.rowCount === 0) {
      return res.status(404).json({ success: false, message: "공정을 찾을 수 없습니다." });
    }

    // 같은 changed_at 기준으로 그룹핑하여 차수별 이력 추출
    // total_production_qty가 증가한(new_value > old_value) 로그만 = 실적 등록 시점
    const historyResult = await pool.query(
      `WITH grouped AS (
         SELECT
           changed_at,
           MAX(changed_by) as changed_by,
           MAX(CASE WHEN changed_column = 'total_production_qty' THEN old_value END) as total_old,
           MAX(CASE WHEN changed_column = 'total_production_qty' THEN new_value END) as total_new,
           MAX(CASE WHEN changed_column = 'good_qty' THEN old_value END) as good_old,
           MAX(CASE WHEN changed_column = 'good_qty' THEN new_value END) as good_new,
           MAX(CASE WHEN changed_column = 'defect_qty' THEN old_value END) as defect_old,
           MAX(CASE WHEN changed_column = 'defect_qty' THEN new_value END) as defect_new
         FROM work_order_process_log
         WHERE original_id = $1
           AND changed_column IN ('total_production_qty', 'good_qty', 'defect_qty')
           AND new_value IS NOT NULL
         GROUP BY changed_at
       )
       SELECT * FROM grouped
       WHERE total_new IS NOT NULL
         AND (COALESCE(total_new::int, 0) - COALESCE(total_old::int, 0)) > 0
       ORDER BY changed_at ASC`,
      [work_order_process_id]
    );

    const batches = historyResult.rows.map((row: any, idx: number) => {
      const batchQty = (parseInt(row.total_new, 10) || 0) - (parseInt(row.total_old, 10) || 0);
      const batchGood = (parseInt(row.good_new, 10) || 0) - (parseInt(row.good_old, 10) || 0);
      const batchDefect = (parseInt(row.defect_new, 10) || 0) - (parseInt(row.defect_old, 10) || 0);

      return {
        seq: idx + 1,
        batch_qty: batchQty,
        batch_good: batchGood,
        batch_defect: batchDefect,
        accumulated_total: parseInt(row.total_new, 10) || 0,
        changed_at: row.changed_at,
        changed_by: row.changed_by,
      };
    });

    logger.info("[pop/production] result-history 조회", {
      work_order_process_id,
      batchCount: batches.length,
    });

    return res.json({
      success: true,
      data: batches,
    });
  } catch (error: any) {
    logger.error("[pop/production] result-history 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "이력 조회 중 오류가 발생했습니다.",
    });
  }
};

/**
 * 앞공정 완료량 + 접수가능량 조회
 * GET /api/pop/production/available-qty?work_order_process_id=xxx
 * 반환: { prevGoodQty, myInputQty, availableQty, instructionQty }
 */
export const getAvailableQty = async (req: AuthenticatedRequest, res: Response) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const rawWopId = req.query.work_order_process_id;
    const work_order_process_id = Array.isArray(rawWopId) ? rawWopId[0] : rawWopId;

    if (!work_order_process_id) {
      return res.status(400).json({
        success: false,
        message: "work_order_process_id가 필요합니다.",
      });
    }

    const current = await pool.query(
      `SELECT wop.seq_no, wop.wo_id, wop.parent_process_id,
              wi.qty as instruction_qty
       FROM work_order_process wop
       JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code
       WHERE wop.id = $1 AND wop.company_code = $2`,
      [work_order_process_id, companyCode]
    );

    if (current.rowCount === 0) {
      return res.status(404).json({ success: false, message: "공정을 찾을 수 없습니다." });
    }

    const { seq_no, wo_id, instruction_qty } = current.rows[0];
    const instrQty = parseInt(instruction_qty, 10) || 0;
    const seqNum = parseInt(seq_no, 10);

    // 재작업 카드 여부 확인
    const reworkCheck = await pool.query(
      `SELECT is_rework, input_qty FROM work_order_process WHERE id = $1`,
      [work_order_process_id]
    );
    const isRework = reworkCheck.rows[0]?.is_rework === "Y";

    let myInputQty: number;
    let prevGoodQty: number;
    let availableQty: number;

    if (isRework) {
      // 재작업 카드: 자체 input_qty가 접수 가능 수량
      const reworkInput = parseInt(reworkCheck.rows[0]?.input_qty, 10) || 0;
      myInputQty = 0;
      prevGoodQty = reworkInput;
      availableQty = reworkInput;
    } else {
      // 일반 카드: 앞공정 양품 - 기접수합계 (재작업 카드 제외)
      const totalAccepted = await pool.query(
        `SELECT COALESCE(SUM(input_qty::int), 0) as total_input
         FROM work_order_process
         WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
           AND parent_process_id IS NOT NULL
           AND (is_rework IS NULL OR is_rework != 'Y')`,
        [wo_id, seq_no, companyCode]
      );
      myInputQty = parseInt(totalAccepted.rows[0].total_input, 10) || 0;

      prevGoodQty = instrQty;
      if (seqNum > 1) {
        const prevSeq = String(seqNum - 1);
        const prevProcess = await pool.query(
          `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good
           FROM work_order_process
           WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
             AND parent_process_id IS NOT NULL`,
          [wo_id, prevSeq, companyCode]
        );
        if (prevProcess.rowCount > 0) {
          prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0;
        }
      }
      availableQty = Math.max(0, prevGoodQty - myInputQty);
    }

    logger.info("[pop/production] available-qty 조회", {
      work_order_process_id,
      prevGoodQty,
      myInputQty,
      availableQty,
      instructionQty: instrQty,
    });

    // 앞공정에서 리워크로 완료된 양품 수량 (마크 표시용)
    // rework_source_id별로 개별 추적하여 정확한 미소진 리워크 수량 계산
    let reworkAvailableQty = 0;
    if (!isRework && seqNum > 1) {
      const prevSeq = String(seqNum - 1);
      // 앞공정의 리워크 완료 SPLIT들 (rework_source_id별)
      const reworkSplits = await pool.query(
        `SELECT rework_source_id, COALESCE(SUM(good_qty::int), 0) as rg
         FROM work_order_process
         WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
           AND parent_process_id IS NOT NULL
           AND is_rework = 'Y'
           AND status = 'completed'
           AND good_qty::int > 0
         GROUP BY rework_source_id`,
        [wo_id, prevSeq, companyCode]
      );
      // 현재 공정에서 각 rework_source_id별로 소비된 수량
      for (const rs of reworkSplits.rows) {
        const srcId = rs.rework_source_id;
        const srcGood = parseInt(rs.rg, 10) || 0;
        const consumedResult = await pool.query(
          `SELECT COALESCE(SUM(input_qty::int), 0) as consumed
           FROM work_order_process
           WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
             AND parent_process_id IS NOT NULL
             AND is_rework = 'Y'
             AND rework_source_id = $4`,
          [wo_id, seq_no, companyCode, srcId]
        );
        const consumed = parseInt(consumedResult.rows[0]?.consumed, 10) || 0;
        reworkAvailableQty += Math.max(0, srcGood - consumed);
      }
    }

    return res.json({
      success: true,
      data: {
        prevGoodQty,
        myInputQty,
        availableQty,
        instructionQty: instrQty,
        reworkAvailableQty,  // 리워크 물량 포함 수량
      },
    });
  } catch (error: any) {
    logger.error("[pop/production] available-qty 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "접수가능량 조회 중 오류가 발생했습니다.",
    });
  }
};

/**
 * 공정 접수 (수량 지정)
 * POST /api/pop/production/accept-process
 * body: { work_order_process_id, accept_qty }
 * - 접수 상한 = 앞공정.good_qty - 내.input_qty (첫 공정은 지시수량 - input_qty)
 * - 추가 접수 가능 (in_progress 상태에서도)
 * - status: acceptable/waiting -> in_progress (또는 이미 in_progress면 유지)
 */
export const acceptProcess = async (req: AuthenticatedRequest, res: Response) => {
  const pool = getPool();
  const client = await pool.connect();
  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;
    const { work_order_process_id, accept_qty } = req.body;

    if (!work_order_process_id || !accept_qty) {
      client.release();
      return res.status(400).json({
        success: false,
        message: "work_order_process_id와 accept_qty가 필요합니다.",
      });
    }

    const qty = parseInt(accept_qty, 10);
    if (qty <= 0) {
      client.release();
      return res.status(400).json({ success: false, message: "접수 수량은 1 이상이어야 합니다." });
    }

    await client.query("BEGIN");

    // 원본(마스터) 행 조회 + FOR UPDATE (동시 접수 방지)
    const current = await client.query(
      `SELECT wop.id, wop.seq_no, wop.wo_id, wop.status, wop.parent_process_id,
              wop.process_code, wop.process_name, wop.is_required, wop.is_fixed_order,
              wop.standard_time, wop.equipment_code, wop.routing_detail_id,
              wi.qty as instruction_qty
       FROM work_order_process wop
       JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code
       WHERE wop.id = $1 AND wop.company_code = $2
       FOR UPDATE OF wop`,
      [work_order_process_id, companyCode]
    );

    if (current.rowCount === 0) {
      await client.query("ROLLBACK");
      client.release();
      return res.status(404).json({ success: false, message: "공정을 찾을 수 없습니다." });
    }

    const row = current.rows[0];
    const masterId = row.parent_process_id || row.id;

    if (row.status === "completed") {
      await client.query("ROLLBACK");
      client.release();
      return res.status(400).json({ success: false, message: "이미 완료된 공정입니다." });
    }
    if (row.status !== "acceptable") {
      await client.query("ROLLBACK");
      client.release();
      return res.status(400).json({ success: false, message: `원본 공정 상태(${row.status})에서는 접수할 수 없습니다.` });
    }

    const instrQty = parseInt(row.instruction_qty, 10) || 0;
    const seqNum = parseInt(row.seq_no, 10);

    // 재작업 카드 여부 확인
    const isReworkCard = await client.query(
      `SELECT is_rework, input_qty FROM work_order_process WHERE id = $1`,
      [work_order_process_id]
    );
    const isRework = isReworkCard.rows[0]?.is_rework === "Y";
    const reworkInputQty = parseInt(isReworkCard.rows[0]?.input_qty, 10) || 0;

    let prevGoodQty: number;
    let currentTotalInput: number;
    let availableQty: number;

    if (isRework) {
      // 재작업 카드: 자체 input_qty가 접수 가능 수량 (앞공정과 무관)
      prevGoodQty = reworkInputQty;
      currentTotalInput = 0; // 재작업 카드는 자체가 마스터, 분할 행 없음
      availableQty = reworkInputQty;
    } else {
      // 일반 카드: 앞공정 양품 - 기접수합계
      const totalAccepted = await client.query(
        `SELECT COALESCE(SUM(input_qty::int), 0) as total_input
         FROM work_order_process
         WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
           AND parent_process_id IS NOT NULL
           AND (is_rework IS NULL OR is_rework != 'Y')`,
        [row.wo_id, row.seq_no, companyCode]
      );
      currentTotalInput = parseInt(totalAccepted.rows[0].total_input, 10) || 0;

      prevGoodQty = instrQty;
      if (seqNum > 1) {
        const prevSeq = String(seqNum - 1);
        const prevProcess = await client.query(
          `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good
           FROM work_order_process
           WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
             AND parent_process_id IS NOT NULL`,
          [row.wo_id, prevSeq, companyCode]
        );
        if (prevProcess.rowCount > 0) {
          prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0;
        }
      }
      availableQty = prevGoodQty - currentTotalInput;
    }

    if (qty > availableQty) {
      await client.query("ROLLBACK");
      client.release();
      return res.status(400).json({
        success: false,
        message: `접수가능량(${availableQty})을 초과합니다. (앞공정 완료: ${prevGoodQty}, 기접수합계: ${currentTotalInput})`,
      });
    }

    // batch_id: 컬럼이 있으면 포함, 없으면 제외
    const batchId = req.body.batch_id || `BATCH-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
    const hasBatchCol = _batchMigrationDone;

    // 리워크 정보 전달: 리워크 카드 접수 / 프론트 전달 / 자동 감지
    let splitIsRework: string | null = null;
    let splitReworkSourceId: string | null = null;

    if (isRework) {
      // 케이스 1: 리워크 카드에서 직접 접수
      const parentReworkInfo = await client.query(
        `SELECT is_rework, rework_source_id FROM work_order_process WHERE id = $1`, [work_order_process_id]
      );
      splitIsRework = parentReworkInfo?.rows[0]?.is_rework || null;
      splitReworkSourceId = parentReworkInfo?.rows[0]?.rework_source_id || null;
    } else if (req.body.rework_source_id) {
      // 케이스 2: 프론트에서 리워크 추적 정보 전달
      splitIsRework = "Y";
      splitReworkSourceId = req.body.rework_source_id;
    } else if (seqNum > 1) {
      // 케이스 3: 자동 감지 — 앞공정에서 리워크로 완료된 양품이 있는지 확인
      const prevSeq = String(seqNum - 1);
      // rework_source_id별로 개별 추적
      const prevReworkSplits = await client.query(
        `SELECT rework_source_id, COALESCE(SUM(good_qty::int), 0) as rework_good
         FROM work_order_process
         WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
           AND parent_process_id IS NOT NULL
           AND is_rework = 'Y'
           AND status = 'completed'
           AND good_qty::int > 0
         GROUP BY rework_source_id`,
        [row.wo_id, prevSeq, companyCode]
      );

      // 각 rework_source별로 미소진 수량 확인
      for (const rs of prevReworkSplits.rows) {
        const srcId = rs.rework_source_id;
        const srcGood = parseInt(rs.rework_good, 10) || 0;
        const consumedResult = await client.query(
          `SELECT COALESCE(SUM(input_qty::int), 0) as consumed
           FROM work_order_process
           WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
             AND parent_process_id IS NOT NULL
             AND is_rework = 'Y'
             AND rework_source_id = $4`,
          [row.wo_id, row.seq_no, companyCode, srcId]
        );
        const consumed = parseInt(consumedResult.rows[0]?.consumed, 10) || 0;
        const remaining = srcGood - consumed;

        if (remaining > 0 && qty <= remaining) {
          // 합류 판정: 일반 물량이 있으면 합류(마크 없음), 없으면 마크 부착
          const normalAvailable = availableQty - remaining;
          if (normalAvailable <= 0) {
            // 일반 물량 없음 → 합류 불가 → 리워크 마크
            splitIsRework = "Y";
            splitReworkSourceId = srcId;
          }
          // normalAvailable > 0 → 합류 가능 → 마크 없음 (splitIsRework = null)
          break;
        }
      }
    }

    // 분할 행 INSERT (batch_id는 컬럼 존재 시에만, 리워크 정보 포함)
    const reworkCols = splitIsRework ? ", is_rework, rework_source_id" : "";
    const reworkVals = splitIsRework ? `, $${hasBatchCol ? 15 : 14}, $${hasBatchCol ? 16 : 15}` : "";
    const reworkParams = splitIsRework ? [splitIsRework, splitReworkSourceId] : [];

    const insertCols = `id, wo_id, seq_no, process_code, process_name, is_required, is_fixed_order,
        standard_time, equipment_code, routing_detail_id,
        status, input_qty, good_qty, defect_qty, total_production_qty,
        result_status, accepted_by, accepted_at, started_at,
        parent_process_id, company_code, writer${hasBatchCol ? ", batch_id" : ""}${reworkCols}`;
    const insertVals = `gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9,
        'in_progress', $10, '0', '0', '0',
        'draft', $11, NOW()::text, NOW()::text,
        $12, $13, $11${hasBatchCol ? ", $14" : ""}${reworkVals}`;
    const insertParams = [
      row.wo_id, row.seq_no, row.process_code, row.process_name,
      row.is_required, row.is_fixed_order, row.standard_time,
      row.equipment_code, row.routing_detail_id,
      String(qty), userId, masterId, companyCode,
      ...(hasBatchCol ? [batchId] : []),
      ...reworkParams,
    ];

    const result = await client.query(
      `INSERT INTO work_order_process (${insertCols}) VALUES (${insertVals})
       RETURNING id, input_qty, status, process_name, result_status, accepted_by`,
      insertParams
    );

    // 분할 행에 체크리스트 복사
    const splitId = result.rows[0].id;
    const checklistCount = await copyChecklistToSplit(
      client, masterId, splitId, row.routing_detail_id, companyCode, userId
    );

    // 마스터 행의 input_qty를 분할 합계로 갱신 (리워크 접수 시에는 마스터 input_qty 변경 안 함)
    let newTotalInput = currentTotalInput + qty;
    if (!isRework) {
      await client.query(
        `UPDATE work_order_process SET input_qty = $3, updated_date = NOW()
         WHERE id = $1 AND company_code = $2`,
        [masterId, companyCode, String(newTotalInput)]
      );
    } else {
      newTotalInput = currentTotalInput; // 리워크는 기존 합계 유지
      // 리워크 카드: 전량 접수 시에만 이 카드만 completed로 변경
      // (다른 리워크 카드에 영향 없도록 id 정확히 지정)
      const reworkAlreadyAccepted = await client.query(
        `SELECT COALESCE(SUM(input_qty::int), 0) as total
         FROM work_order_process
         WHERE parent_process_id = $1 AND company_code = $2`,
        [work_order_process_id, companyCode]
      );
      const totalReworkAccepted = (parseInt(reworkAlreadyAccepted.rows[0]?.total, 10) || 0) + qty;
      if (totalReworkAccepted >= reworkInputQty) {
        await client.query(
          `UPDATE work_order_process SET status = 'completed', updated_date = NOW()
           WHERE id = $1 AND company_code = $2`,
          [work_order_process_id, companyCode]
        );
      }
    }

    await client.query("COMMIT");

    logger.info("[pop/production] accept-process 분할 접수 완료", {
      companyCode, userId, masterId,
      splitId,
      acceptedQty: qty,
      totalAccepted: newTotalInput,
      prevGoodQty,
      checklistCount,
    });

    const acceptData = result.rows[0] || {};
    if (splitReworkSourceId) {
      acceptData.rework_source_id = splitReworkSourceId;
      acceptData.is_rework = splitIsRework;
    }

    return res.json({
      success: true,
      data: acceptData,
      message: `${qty}개 접수 완료 (총 접수합계: ${newTotalInput})`,
    });
  } catch (error: any) {
    await client.query("ROLLBACK").catch(() => {});
    logger.error("[pop/production] accept-process 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "접수 중 오류가 발생했습니다.",
    });
  } finally {
    client.release();
  }
};

/**
 * 접수 취소: input_qty를 0으로 리셋하고 status를 acceptable로 되돌림
 * 조건: 아직 실적(total_production_qty)이 없어야 함
 */
export const cancelAccept = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();

  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;
    const { work_order_process_id } = req.body;

    if (!work_order_process_id) {
      return res.status(400).json({
        success: false,
        message: "work_order_process_id는 필수입니다.",
      });
    }

    const current = await pool.query(
      `SELECT id, status, input_qty, total_production_qty, result_status,
              parent_process_id, wo_id, seq_no, process_name,
              target_warehouse_id, target_location_code, good_qty, concession_qty
       FROM work_order_process
       WHERE id = $1 AND company_code = $2`,
      [work_order_process_id, companyCode]
    );

    if (current.rowCount === 0) {
      return res.status(404).json({ success: false, message: "공정을 찾을 수 없습니다." });
    }

    const proc = current.rows[0];

    // 분할 행만 취소 가능 (원본 행은 취소 대상이 아님)
    if (!proc.parent_process_id) {
      return res.status(400).json({
        success: false,
        message: "원본 공정은 접수 취소할 수 없습니다. 분할된 접수 카드에서 취소해주세요.",
      });
    }

    if (proc.status !== "in_progress") {
      return res.status(400).json({
        success: false,
        message: `현재 상태(${proc.status})에서는 접수 취소할 수 없습니다. 진행중 상태만 가능합니다.`,
      });
    }

    const totalProduced = parseInt(proc.total_production_qty ?? "0", 10) || 0;
    const currentInputQty = parseInt(proc.input_qty ?? "0", 10) || 0;
    const unproducedQty = currentInputQty - totalProduced;

    if (unproducedQty <= 0) {
      return res.status(400).json({
        success: false,
        message: "취소할 미소진 접수분이 없습니다. 모든 접수량에 대해 실적이 등록되었습니다.",
      });
    }

    let cancelledQty = unproducedQty;
    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      if (totalProduced === 0) {
        // 실적이 없으면 체크리스트 먼저 삭제 → 분할 행 삭제
        await client.query(
          `DELETE FROM process_work_result WHERE work_order_process_id = $1 AND company_code = $2`,
          [work_order_process_id, companyCode]
        );
        await client.query(
          `DELETE FROM work_order_process WHERE id = $1 AND company_code = $2`,
          [work_order_process_id, companyCode]
        );
      } else {
        // 실적이 있으면 input_qty를 실적 수량으로 축소 + completed
        await client.query(
          `UPDATE work_order_process
           SET input_qty = $3, status = 'completed', result_status = 'confirmed',
               completed_at = NOW()::text, completed_by = $4,
               updated_date = NOW(), writer = $4
           WHERE id = $1 AND company_code = $2`,
          [work_order_process_id, companyCode, String(totalProduced), userId]
        );
      }

      // 재고 원복: 분할 행에 target_warehouse_id가 있으면 입고된 수량을 차감
      if (proc.target_warehouse_id) {
        const inboundQty = parseInt(proc.good_qty || "0", 10) + parseInt(proc.concession_qty || "0", 10);
        if (inboundQty > 0) {
          // work_instruction에서 item_id 조회
          const wiResult = await client.query(
            `SELECT item_id FROM work_instruction WHERE id = $1 AND company_code = $2`,
            [proc.wo_id, companyCode]
          );
          if (wiResult.rowCount > 0) {
            const itemResult = await client.query(
              `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`,
              [wiResult.rows[0].item_id, companyCode]
            );
            if (itemResult.rowCount > 0) {
              const itemCode = itemResult.rows[0].item_number;
              const locCode = proc.target_location_code || proc.target_warehouse_id;
              await client.query(
                `UPDATE inventory_stock
                 SET current_qty = GREATEST((COALESCE(current_qty::numeric, 0) - $4::numeric), 0)::text,
                     updated_date = NOW(), writer = $5
                 WHERE company_code = $1 AND item_code = $2
                   AND warehouse_code = $3 AND location_code = $6`,
                [companyCode, itemCode, proc.target_warehouse_id, String(inboundQty), userId, locCode]
              );
            }
          }
        }
      }

      // 마스터 행의 input_qty를 분할 합계로 재계산
      const remainingSplits = await client.query(
        `SELECT COALESCE(SUM(input_qty::int), 0) as total_input
         FROM work_order_process
         WHERE parent_process_id = $1 AND company_code = $2`,
        [proc.parent_process_id, companyCode]
      );
      const newMasterInput = parseInt(remainingSplits.rows[0].total_input, 10) || 0;

      // 원본(마스터) 행: input_qty 복원 + acceptable 상태 유지
      await client.query(
        `UPDATE work_order_process
         SET status = 'acceptable', input_qty = $3, updated_date = NOW()
         WHERE id = $1 AND company_code = $2 AND parent_process_id IS NULL`,
        [proc.parent_process_id, companyCode, String(newMasterInput)]
      );

      await client.query("COMMIT");
    } catch (txErr) {
      await client.query("ROLLBACK");
      throw txErr;
    } finally {
      client.release();
    }

    logger.info("[pop/production] cancel-accept 완료 (분할 행)", {
      companyCode, userId, work_order_process_id,
      masterId: proc.parent_process_id,
      previousInputQty: currentInputQty,
      totalProduced,
      cancelledQty,
      action: totalProduced === 0 ? "DELETE" : "SHRINK",
    });

    return res.json({
      success: true,
      data: { id: work_order_process_id, process_name: proc.process_name },
      message: `미소진 ${cancelledQty}개 접수가 취소되었습니다.`,
    });
  } catch (error: any) {
    logger.error("[pop/production] cancel-accept 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "접수 취소 중 오류가 발생했습니다.",
    });
  }
};

/**
 * 창고 목록 조회 (POP 생산용)
 */
export const getWarehouses = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const result = await pool.query(
      `SELECT id, warehouse_code, warehouse_name, warehouse_type
       FROM warehouse_info
       WHERE company_code = $1 AND COALESCE(status, '') != '삭제'
       ORDER BY warehouse_name`,
      [companyCode]
    );
    return res.json({ success: true, data: result.rows });
  } catch (error: any) {
    logger.error("[pop/production] 창고 목록 조회 실패:", error);
    return res.status(500).json({ success: false, message: error.message });
  }
};

/**
 * 특정 창고의 위치(로케이션) 목록 조회
 * warehouseId는 warehouse_info.id → warehouse_code를 조회해서 warehouse_location과 매칭
 */
export const getWarehouseLocations = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const { warehouseId } = req.params;
    if (!warehouseId) {
      return res.status(400).json({ success: false, message: "warehouseId는 필수입니다." });
    }

    // warehouse_info.id → warehouse_code 변환
    const whInfo = await pool.query(
      `SELECT warehouse_code FROM warehouse_info WHERE id = $1 AND company_code = $2`,
      [warehouseId, companyCode]
    );
    if (whInfo.rowCount === 0) {
      return res.json({ success: true, data: [] });
    }
    const warehouseCode = whInfo.rows[0].warehouse_code;

    const result = await pool.query(
      `SELECT id, location_code, location_name
       FROM warehouse_location
       WHERE warehouse_code = $1 AND company_code = $2
       ORDER BY location_name`,
      [warehouseCode, companyCode]
    );
    return res.json({ success: true, data: result.rows });
  } catch (error: any) {
    logger.error("[pop/production] 창고 위치 조회 실패:", error);
    return res.status(500).json({ success: false, message: error.message });
  }
};

/**
 * 마지막 공정 여부 확인
 * 같은 wo_id에서 현재 seq_no보다 큰 공정(마스터 행)이 없으면 마지막
 */
export const isLastProcess = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const { processId } = req.params;
    if (!processId) {
      return res.json({ success: true, data: { isLast: false } });
    }

    // 현재 공정의 wo_id와 seq_no 조회 (분할 행이면 parent의 seq_no 기준)
    const process = await pool.query(
      `SELECT wo_id, seq_no, parent_process_id
       FROM work_order_process
       WHERE id = $1 AND company_code = $2`,
      [processId, companyCode]
    );
    if (process.rowCount === 0) {
      return res.json({ success: true, data: { isLast: false } });
    }

    const { wo_id, seq_no, parent_process_id } = process.rows[0];

    // 분할 행이면 마스터의 seq_no 기준으로 판단
    let effectiveSeqNo = seq_no;
    if (parent_process_id) {
      const master = await pool.query(
        `SELECT seq_no FROM work_order_process WHERE id = $1 AND company_code = $2`,
        [parent_process_id, companyCode]
      );
      if (master.rowCount > 0) {
        effectiveSeqNo = master.rows[0].seq_no;
      }
    }

    const next = await pool.query(
      `SELECT id FROM work_order_process
       WHERE wo_id = $1 AND company_code = $2
         AND CAST(seq_no AS int) > CAST($3 AS int)
         AND parent_process_id IS NULL
       LIMIT 1`,
      [wo_id, companyCode, effectiveSeqNo]
    );

    // 현재 공정의 기존 창고 설정도 반환 (기본값 세팅용)
    const warehouseInfo = await pool.query(
      `SELECT target_warehouse_id, target_location_code
       FROM work_order_process
       WHERE id = $1 AND company_code = $2`,
      [processId, companyCode]
    );

    return res.json({
      success: true,
      data: {
        isLast: next.rowCount === 0,
        woId: wo_id,
        seqNo: effectiveSeqNo,
        targetWarehouseId: warehouseInfo.rows[0]?.target_warehouse_id || null,
        targetLocationCode: warehouseInfo.rows[0]?.target_location_code || null,
      },
    });
  } catch (error: any) {
    logger.error("[pop/production] 마지막 공정 확인 오류:", error);
    return res.status(500).json({ success: false, message: error.message });
  }
};

/**
 * 공정의 목표 창고/위치 업데이트
 * 마지막 공정 완료 전 또는 완료 후 창고를 지정한다.
 * 마스터 행에 저장하여 checkAndCompleteWorkInstruction이 참조할 수 있도록 한다.
 */
export const updateTargetWarehouse = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;
    const { work_order_process_id, target_warehouse_id, target_location_code } = req.body;

    if (!work_order_process_id || !target_warehouse_id) {
      return res.status(400).json({
        success: false,
        message: "work_order_process_id와 target_warehouse_id는 필수입니다.",
      });
    }

    // 분할 행이면 마스터 행도 함께 업데이트
    const procInfo = await pool.query(
      `SELECT parent_process_id FROM work_order_process WHERE id = $1 AND company_code = $2`,
      [work_order_process_id, companyCode]
    );

    const idsToUpdate = [work_order_process_id];
    if (procInfo.rowCount > 0 && procInfo.rows[0].parent_process_id) {
      idsToUpdate.push(procInfo.rows[0].parent_process_id);
    }

    for (const id of idsToUpdate) {
      await pool.query(
        `UPDATE work_order_process
         SET target_warehouse_id = $3,
             target_location_code = $4,
             writer = $5,
             updated_date = NOW()
         WHERE id = $1 AND company_code = $2`,
        [id, companyCode, target_warehouse_id, target_location_code || null, userId]
      );
    }

    logger.info("[pop/production] 목표 창고 업데이트", {
      companyCode, userId, work_order_process_id,
      target_warehouse_id, target_location_code,
      updatedIds: idsToUpdate,
    });

    return res.json({ success: true, data: { target_warehouse_id, target_location_code } });
  } catch (error: any) {
    logger.error("[pop/production] 목표 창고 업데이트 오류:", error);
    return res.status(500).json({ success: false, message: error.message });
  }
};

/**
 * 독립 재고 입고 API
 * 창고 저장 + inventory_stock UPSERT를 한 번에 수행한다.
 * 실적(save-result) 완료 후 나중에 창고를 선택해도 재고가 들어가도록 분리.
 * 이중 입고 방지: target_warehouse_id가 이미 설정된 경우 "이미 입고됨" 반환.
 */
export const inventoryInbound = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();
  const client = await pool.connect();

  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;
    const { work_order_process_id, warehouse_code, location_code } = req.body;

    if (!work_order_process_id || !warehouse_code) {
      return res.status(400).json({
        success: false,
        message: "work_order_process_id와 warehouse_code는 필수입니다.",
      });
    }

    await client.query("BEGIN");

    // 1. work_order_process에서 wo_id, good_qty, parent_process_id, 기존 target_warehouse_id 조회
    const procResult = await client.query(
      `SELECT wo_id, good_qty, concession_qty, parent_process_id, target_warehouse_id, seq_no, is_rework
       FROM work_order_process
       WHERE id = $1 AND company_code = $2`,
      [work_order_process_id, companyCode]
    );

    if (procResult.rowCount === 0) {
      await client.query("ROLLBACK");
      return res.status(404).json({
        success: false,
        message: "해당 공정을 찾을 수 없습니다.",
      });
    }

    const proc = procResult.rows[0];

    // 이중 입고 방지: 이미 target_warehouse_id가 설정되어 있으면 거부
    if (proc.target_warehouse_id) {
      await client.query("ROLLBACK");
      return res.status(409).json({
        success: false,
        message: "이미 재고 입고가 완료된 공정입니다.",
        data: { existing_warehouse: proc.target_warehouse_id },
      });
    }

    const goodQty = parseInt(proc.good_qty || "0", 10) + parseInt(proc.concession_qty || "0", 10);

    if (goodQty <= 0) {
      await client.query("ROLLBACK");
      return res.status(400).json({
        success: false,
        message: "양품 수량이 0이므로 재고 입고할 수 없습니다.",
      });
    }

    // 2. work_instruction에서 item_id 조회
    const wiResult = await client.query(
      `SELECT item_id FROM work_instruction WHERE id = $1 AND company_code = $2`,
      [proc.wo_id, companyCode]
    );

    if (wiResult.rowCount === 0) {
      await client.query("ROLLBACK");
      return res.status(404).json({
        success: false,
        message: "작업지시를 찾을 수 없습니다.",
      });
    }

    const itemId = wiResult.rows[0].item_id;

    // 3. item_info에서 item_number 조회
    const itemResult = await client.query(
      `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`,
      [itemId, companyCode]
    );

    if (itemResult.rowCount === 0) {
      await client.query("ROLLBACK");
      return res.status(404).json({
        success: false,
        message: "품목 정보를 찾을 수 없습니다.",
      });
    }

    const itemCode = itemResult.rows[0].item_number;
    const effectiveLocationCode = location_code || null;

    // 4. inventory_stock UPSERT (PC receivingController와 동일한 SELECT→INSERT/UPDATE 패턴)
    await upsertInventoryStock(client, companyCode, itemCode, warehouse_code, effectiveLocationCode, goodQty, userId);

    // 5. work_order_process에 target_warehouse_id 저장 (현재 행 + 마스터 행)
    const idsToUpdate = [work_order_process_id];
    if (proc.parent_process_id) {
      idsToUpdate.push(proc.parent_process_id);
    }

    for (const id of idsToUpdate) {
      await client.query(
        `UPDATE work_order_process
         SET target_warehouse_id = $3,
             target_location_code = $4,
             writer = $5,
             updated_date = NOW()
         WHERE id = $1 AND company_code = $2`,
        [id, companyCode, warehouse_code, location_code || null, userId]
      );
    }

    // 6. 리워크 마크 해제 (창고 입고 = 정상 제품 인정, 이력은 rework_source_id에 영구 보존)
    if (proc.is_rework === "Y") {
      await client.query(
        `UPDATE work_order_process SET is_rework = NULL, updated_date = NOW()
         WHERE id = $1 AND company_code = $2`,
        [work_order_process_id, companyCode]
      );
    }

    await client.query("COMMIT");

    logger.info("[pop/production] 독립 재고 입고 완료", {
      companyCode, userId, work_order_process_id,
      itemCode, warehouse_code, location_code: effectiveLocationCode,
      qty: goodQty,
      reworkCleared: proc.is_rework === "Y",
    });

    return res.json({
      success: true,
      message: "재고 입고가 완료되었습니다.",
      data: {
        item_code: itemCode,
        warehouse_code,
        location_code: effectiveLocationCode,
        qty: goodQty,
      },
    });
  } catch (error: any) {
    await client.query("ROLLBACK").catch(() => {});
    logger.error("[pop/production] 독립 재고 입고 오류:", error);
    return res.status(500).json({ success: false, message: error.message });
  } finally {
    client.release();
  }
};

/**
 * 간이 재고 입고 (공정 접수 없이 바로 입고)
 * 품목 + 수량 + 창고만으로 inventory_stock UPSERT + inbound_mng 이력 기록
 */
export const quickInventoryInbound = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();
  const client = await pool.connect();

  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;
    const { item_id, qty, warehouse_code, location_code, remark } = req.body;

    // 필수 파라미터 검증
    if (!item_id || !qty || !warehouse_code) {
      return res.status(400).json({
        success: false,
        message: "item_id, qty, warehouse_code는 필수입니다.",
      });
    }

    const parsedQty = parseInt(String(qty), 10);
    if (isNaN(parsedQty) || parsedQty <= 0) {
      return res.status(400).json({
        success: false,
        message: "수량은 1 이상의 정수여야 합니다.",
      });
    }

    await client.query("BEGIN");

    // 1. item_info에서 item_number, item_name 조회
    const itemResult = await client.query(
      `SELECT item_number, item_name, size, material, unit
       FROM item_info WHERE id = $1 AND company_code = $2`,
      [item_id, companyCode]
    );

    if (itemResult.rowCount === 0) {
      await client.query("ROLLBACK");
      return res.status(404).json({
        success: false,
        message: "품목 정보를 찾을 수 없습니다.",
      });
    }

    const item = itemResult.rows[0];
    const itemCode = item.item_number;
    const effectiveLocationCode = location_code || null;

    // 2. inventory_stock UPSERT (PC receivingController와 동일한 SELECT→INSERT/UPDATE 패턴)
    await upsertInventoryStock(client, companyCode, itemCode, warehouse_code, effectiveLocationCode, parsedQty, userId);

    // 3. inbound_mng에 간이입고 이력 기록
    const seqResult = await client.query(
      `SELECT COALESCE(MAX(
        CASE WHEN inbound_number ~ '^QIB-[0-9]{4}-[0-9]+$'
             THEN CAST(SUBSTRING(inbound_number FROM '[0-9]+$') AS INTEGER)
             ELSE 0 END
       ), 0) + 1 AS next_seq
       FROM inbound_mng WHERE company_code = $1`,
      [companyCode]
    );
    const nextSeq = seqResult.rows[0].next_seq;
    const year = new Date().getFullYear();
    const inboundNumber = `QIB-${year}-${String(nextSeq).padStart(4, "0")}`;

    await client.query(
      `INSERT INTO inbound_mng (
        id, company_code, inbound_number, inbound_type, inbound_date,
        item_number, item_name, spec, material, unit,
        inbound_qty, warehouse_code, location_code,
        inbound_status, memo, remark,
        created_date, updated_date, writer, created_by, updated_by
      ) VALUES (
        gen_random_uuid()::text, $1, $2, '간이입고', CURRENT_DATE,
        $3, $4, $5, $6, $7,
        $8, $9, $10,
        '완료', $11, $12,
        NOW(), NOW(), $13, $13, $13
      )`,
      [
        companyCode, inboundNumber,
        item.item_number, item.item_name, item.size, item.material, item.unit,
        parsedQty, warehouse_code, effectiveLocationCode,
        remark || "POP 간이입고", remark || null,
        userId,
      ]
    );

    await client.query("COMMIT");

    logger.info("[pop/production] 간이 재고 입고 완료", {
      companyCode, userId, item_id,
      itemCode, warehouse_code, location_code: effectiveLocationCode,
      qty: parsedQty, inboundNumber,
    });

    return res.json({
      success: true,
      message: "간이 재고 입고가 완료되었습니다.",
      data: {
        inbound_number: inboundNumber,
        item_code: itemCode,
        item_name: item.item_name,
        warehouse_code,
        location_code: effectiveLocationCode,
        qty: parsedQty,
      },
    });
  } catch (error: any) {
    await client.query("ROLLBACK").catch(() => {});
    logger.error("[pop/production] 간이 재고 입고 오류:", error);
    return res.status(500).json({ success: false, message: error.message });
  } finally {
    client.release();
  }
};

/**
 * 재작업 이력 조회
 * 작업지시(wo_id) 기준으로 모든 재작업 체인을 반환한다.
 * 원본 → 재작업1 → 재작업2 → ... 순서로 체인 추적.
 */
export const getReworkHistory = async (req: AuthenticatedRequest, res: Response) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const woId = req.query.wo_id as string || req.params.woId;
    if (!woId) {
      return res.status(400).json({ success: false, message: "wo_id는 필수입니다." });
    }

    const result = await pool.query(
      `SELECT id, seq_no, process_code, process_name, status,
              input_qty, good_qty, defect_qty, concession_qty,
              is_rework, rework_source_id, parent_process_id,
              accepted_by, accepted_at, started_at, completed_at,
              created_date
       FROM work_order_process
       WHERE wo_id = $1 AND company_code = $2
         AND (is_rework = 'Y' OR is_rework = '1' OR defect_qty::int > 0 OR parent_process_id IS NOT NULL)
       ORDER BY created_date ASC`,
      [woId, companyCode]
    );

    // 체인 구성: rework_source_id를 따라 트리 구조
    const rows = result.rows;
    const byId: Record<string, typeof rows[0]> = {};
    for (const r of rows) byId[r.id] = r;

    const chains: Array<{
      source: typeof rows[0];
      reworks: typeof rows;
      totalReworkCount: number;
    }> = [];

    // 원본 행(불량 발생한 것) 찾기
    const reworkSourceIds = new Set(rows.filter(r => r.rework_source_id).map(r => r.rework_source_id));
    const sources = rows.filter(r => reworkSourceIds.has(r.id) || (parseInt(r.defect_qty || "0", 10) > 0 && r.is_rework !== "Y"));

    for (const src of sources) {
      const chain: typeof rows = [];
      const visited = new Set<string>();
      // 이 소스에서 시작하는 재작업 체인 추적
      const queue = rows.filter(r => r.rework_source_id === src.id);
      while (queue.length > 0) {
        const item = queue.shift()!;
        if (visited.has(item.id)) continue;
        visited.add(item.id);
        chain.push(item);
        // 이 재작업에서 또 재작업이 나온 것 찾기
        const next = rows.filter(r => r.rework_source_id === item.id);
        queue.push(...next);
      }
      chains.push({
        source: src,
        reworks: chain,
        totalReworkCount: chain.length,
      });
    }

    return res.json({
      success: true,
      data: {
        wo_id: woId,
        total_rework_count: rows.filter(r => r.is_rework === "Y" || r.is_rework === "1").length,
        chains,
        all_records: rows,
      },
    });
  } catch (error: any) {
    logger.error("[pop/production] rework-history 오류:", error);
    return res.status(500).json({ success: false, message: error.message });
  }
};

/**
 * 공정별 BOM 자재 목록 + 소요량 계산
 * work_order_process_id → item_code → bom + bom_detail 조회
 */
export const getBomMaterials = async (req: AuthenticatedRequest, res: Response) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const { processId } = req.params;
    if (!processId) {
      return res.status(400).json({ success: false, message: "processId 필수" });
    }

    // 1. work_order_process → work_instruction → item_code, plan_qty
    const procResult = await pool.query(
      `SELECT wop.wo_id, wop.process_code, wop.input_qty, wop.plan_qty,
              wi.item_id, wi.qty as instruction_qty
       FROM work_order_process wop
       JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code
       WHERE wop.id = $1 AND wop.company_code = $2`,
      [processId, companyCode]
    );
    if (procResult.rowCount === 0) {
      return res.json({ success: true, data: { materials: [], processQty: 0 } });
    }
    const proc = procResult.rows[0];
    const processQty = parseInt(proc.input_qty || proc.plan_qty || proc.instruction_qty || "0", 10);

    // 2. item_info → item_code (item_number)
    const itemResult = await pool.query(
      `SELECT item_number, item_name FROM item_info WHERE id = $1 AND company_code = $2`,
      [proc.item_id, companyCode]
    );
    if (itemResult.rowCount === 0) {
      return res.json({ success: true, data: { materials: [], processQty } });
    }
    const itemCode = itemResult.rows[0].item_number;

    // 3. BOM 조회
    const bomResult = await pool.query(
      `SELECT bd.id, bd.child_item_id, bd.quantity, bd.unit, bd.process_type, bd.loss_rate,
              i.item_name as child_item_name, i.item_number as child_item_code, i.unit as item_unit
       FROM bom b
       JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code
       LEFT JOIN item_info i ON bd.child_item_id = i.id AND i.company_code = b.company_code
       WHERE (b.item_code = $1 OR b.item_id = $2) AND b.company_code = $3
       ORDER BY bd.seq_no ASC`,
      [itemCode, proc.item_id, companyCode]
    );

    // 4. 소요량 계산
    const bomBase = await pool.query(
      `SELECT base_qty FROM bom WHERE (item_code = $1 OR item_id = $2) AND company_code = $3 LIMIT 1`,
      [itemCode, proc.item_id, companyCode]
    );
    const baseQty = parseFloat(bomBase.rows[0]?.base_qty || "1") || 1;

    // 기존 투입량 조회 (child_item_id별 합산)
    const inputResult = await pool.query(
      `SELECT detail_content as child_item_id, SUM(CAST(NULLIF(result_value, '') AS numeric)) as total_input
       FROM process_work_result
       WHERE work_order_process_id = $1 AND company_code = $2 AND detail_type = 'material_input'
       GROUP BY detail_content`,
      [processId, companyCode]
    );
    const inputMap = new Map<string, number>();
    for (const row of inputResult.rows) {
      inputMap.set(String(row.child_item_id), parseFloat(row.total_input) || 0);
    }

    const materials = bomResult.rows.map((bd: Record<string, unknown>) => {
      const bomQty = parseFloat(String(bd.quantity || "0")) || 0;
      const lossRate = parseFloat(String(bd.loss_rate || "0")) || 0;
      const requiredQty = Math.ceil((processQty / baseQty) * bomQty * (1 + lossRate / 100));
      const childItemId = String(bd.child_item_id || "");
      return {
        id: bd.id,
        child_item_id: childItemId,
        child_item_code: bd.child_item_code || "",
        child_item_name: bd.child_item_name || "",
        bom_qty: bomQty,
        unit: bd.unit || bd.item_unit || "",
        process_type: bd.process_type || "",
        loss_rate: lossRate,
        required_qty: requiredQty,
        input_qty: inputMap.get(childItemId) || 0,
      };
    });

    return res.json({
      success: true,
      data: { materials, processQty, baseQty, itemCode, itemName: itemResult.rows[0].item_name },
    });
  } catch (error: any) {
    logger.error("[pop/production] bom-materials 오류:", error);
    return res.status(500).json({ success: false, message: error.message });
  }
};

/**
 * 자재 투입 기록 저장
 * BOM 기준과 다른 수량도 허용 (유동 투입)
 */
export const saveMaterialInput = async (req: AuthenticatedRequest, res: Response) => {
  const pool = getPool();
  const client = await pool.connect();
  try {
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;
    const { work_order_process_id, inputs } = req.body;

    if (!work_order_process_id || !inputs || !Array.isArray(inputs)) {
      return res.status(400).json({ success: false, message: "work_order_process_id, inputs[] 필수" });
    }

    await client.query("BEGIN");

    const results = [];
    for (const input of inputs) {
      const { child_item_id, child_item_code, child_item_name, input_qty, unit, bom_detail_id, required_qty, warehouse_code, location_code } = input;
      // item_code/qty 등 대안 필드명도 허용
      const effectiveItemId = child_item_id || input.item_id || input.item_code || child_item_code;
      const effectiveItemCode = child_item_code || input.item_code || child_item_id;
      const effectiveItemName = child_item_name || input.item_name || "";
      const effectiveQty = input_qty || input.qty || input.quantity;

      if (!effectiveItemId || !effectiveQty) continue;

      const parsedQty = parseFloat(String(effectiveQty));
      if (isNaN(parsedQty) || parsedQty <= 0) continue;

      // 투입 기록 INSERT (process_work_result에 material_input 타입으로)
      const insertResult = await client.query(
        `INSERT INTO process_work_result (
          id, company_code, work_order_process_id,
          detail_type, detail_content, item_title,
          result_value, unit, is_passed, status,
          remark, recorded_by, recorded_at, writer
        ) VALUES (
          gen_random_uuid()::text, $1, $2,
          'material_input', $3, $4,
          $5, $6, 'Y', 'completed',
          $7, $8, NOW()::text, $8
        ) RETURNING id`,
        [
          companyCode, work_order_process_id,
          effectiveItemCode || effectiveItemId, effectiveItemName,
          String(parsedQty), unit || "",
          JSON.stringify({ bom_detail_id, required_qty: required_qty || 0, warehouse_code, location_code }),
          userId,
        ]
      );

      // 재고 차감 (warehouse_code가 있을 때만)
      if (warehouse_code) {
        const locCode = location_code || warehouse_code;
        await client.query(
          `UPDATE inventory_stock
           SET current_qty = (COALESCE(current_qty::numeric, 0) - $4::numeric)::text,
               updated_date = NOW(), writer = $5
           WHERE company_code = $1 AND item_code = $2
             AND warehouse_code = $3 AND location_code = $6`,
          [companyCode, effectiveItemCode, warehouse_code, String(parsedQty), userId, locCode]
        );
      }

      results.push({ id: insertResult.rows[0].id, child_item_code: effectiveItemCode, input_qty: parsedQty });
    }

    await client.query("COMMIT");

    return res.json({
      success: true,
      message: `${results.length}건 자재 투입 완료`,
      data: results,
    });
  } catch (error: any) {
    await client.query("ROLLBACK").catch(() => {});
    logger.error("[pop/production] material-input 오류:", error);
    return res.status(500).json({ success: false, message: error.message });
  } finally {
    client.release();
  }
};

/**
 * 자재 투입 현황 조회
 */
export const getMaterialInputs = async (req: AuthenticatedRequest, res: Response) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const { processId } = req.params;
    if (!processId) {
      return res.status(400).json({ success: false, message: "processId 필수" });
    }

    const result = await pool.query(
      `SELECT id, detail_content as item_code, item_title as item_name,
              result_value as input_qty, unit, remark, recorded_by, recorded_at
       FROM process_work_result
       WHERE work_order_process_id = $1 AND company_code = $2
         AND detail_type = 'material_input'
       ORDER BY recorded_at ASC`,
      [processId, companyCode]
    );

    return res.json({ success: true, data: result.rows });
  } catch (error: any) {
    logger.error("[pop/production] material-inputs 조회 오류:", error);
    return res.status(500).json({ success: false, message: error.message });
  }
};

/**
 * 체크리스트 조회 (judgment_criteria 조인 포함)
 *
 * process_work_result를 조회하면서 inspection_standard.judgment_criteria를
 * LEFT JOIN으로 같이 반환한다.
 *
 * UI는 프론트의 resolveInputType()에서
 *  1순위: judgment_criteria (CAT_JC_01~04)
 *  2순위: detail_type 폴백
 * 으로 입력 UI를 결정한다.
 */
export const getChecklistItems = async (
  req: AuthenticatedRequest,
  res: Response
) => {
  const pool = getPool();
  try {
    const companyCode = req.user!.companyCode;
    const { processId } = req.params;
    if (!processId) {
      return res
        .status(400)
        .json({ success: false, message: "processId 필수" });
    }

    const result = await pool.query(
      `SELECT
          pwr.id,
          pwr.company_code,
          pwr.work_order_process_id,
          pwr.source_work_item_id,
          pwr.source_detail_id,
          pwr.work_phase,
          pwr.item_title,
          pwr.item_sort_order,
          pwr.detail_content,
          pwr.detail_type,
          pwr.detail_sort_order,
          pwr.is_required,
          pwr.inspection_code,
          pwr.inspection_method,
          pwr.unit,
          pwr.lower_limit,
          pwr.upper_limit,
          pwr.input_type,
          pwr.lookup_target,
          pwr.display_fields,
          pwr.duration_minutes,
          pwr.status,
          pwr.result_value,
          pwr.is_passed,
          pwr.remark,
          pwr.recorded_by,
          pwr.recorded_at,
          pwr.started_at,
          pwr.group_started_at,
          pwr.group_paused_at,
          pwr.group_total_paused_time,
          pwr.group_completed_at,
          ist.judgment_criteria
        FROM process_work_result pwr
        LEFT JOIN inspection_standard ist
          ON pwr.inspection_code = ist.inspection_code
         AND pwr.company_code = ist.company_code
        WHERE pwr.work_order_process_id = $1
          AND pwr.company_code = $2
        ORDER BY
          COALESCE(NULLIF(pwr.item_sort_order, '')::int, 0),
          COALESCE(NULLIF(pwr.detail_sort_order, '')::int, 0)`,
      [processId, companyCode]
    );

    return res.json({ success: true, data: result.rows });
  } catch (error: any) {
    logger.error("[pop/production] checklist-items 조회 오류:", error);
    return res
      .status(500)
      .json({ success: false, message: error.message });
  }
};
