import type { Response } from "express";
import type { Pool, PoolClient } from "pg";
import { getPool } from "../database/db";
import type { AuthenticatedRequest } from "../middleware/authMiddleware";
import logger from "../utils/logger";

// 불량 상세 항목 타입
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],
		);
	}
}

/**
 * 앞공정 양품 합계 조회 헬퍼
 * - 첫 공정이면 null 반환 (호출자가 instruction_qty 사용)
 * - 앞공정이 있으면 SUM(good_qty + concession_qty) 반환
 */
async function getPrevProcessGoodQty(
	exec: Pool | PoolClient,
	woId: string,
	seqNum: number,
	companyCode: string,
): Promise<number | null> {
	// 1. 첫 공정 여부 판정
	const minSeqCheck = await exec.query(
		`SELECT MIN(CAST(seq_no AS int)) as min_seq
     FROM work_order_process
     WHERE wo_id = $1 AND company_code = $2`,
		[woId, companyCode],
	);
	const minSeq = parseInt(minSeqCheck.rows[0]?.min_seq, 10) || seqNum;
	if (seqNum <= minSeq) {
		return null; // 첫 공정
	}

	// 2. 앞 seq 조회
	const prevProcessSeq = await exec.query(
		`SELECT MAX(CAST(seq_no AS int)) as prev_seq
     FROM work_order_process
     WHERE wo_id = $1 AND company_code = $2
       AND CAST(seq_no AS int) < $3`,
		[woId, companyCode, seqNum],
	);
	const actualPrevSeq = prevProcessSeq.rows[0]?.prev_seq;
	if (actualPrevSeq == null) {
		return null;
	}

	// 3. 앞공정 양품 SUM
	const prevAgg = await exec.query(
		`SELECT COALESCE(SUM(CAST(NULLIF(wr.good_qty, '') AS int)), 0)
              + COALESCE(SUM(CAST(NULLIF(wr.concession_qty, '') AS int)), 0) as total_good
     FROM work_order_process_result wr
     JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
     WHERE wop.wo_id = $1 AND wop.seq_no = $2 AND wop.company_code = $3
       AND wr.result_status IN ('draft','confirmed')`,
		[woId, String(actualPrevSeq), companyCode],
	);
	return parseInt(prevAgg.rows[0].total_good, 10) || 0;
}

/**
 * work_instruction 헤더 필드를 COALESCE 방식으로 업데이트하는 공통 헬퍼
 * syncWorkInstructions 내 header_routing 단일 경로와 detailRows 다중 경로에서 공용
 */
async function updateWorkInstructionHeader(
	exec: Pool | PoolClient,
	wiId: string,
	companyCode: string,
	routing: string | null,
	qty: string | number | null,
	itemNumber: string | null,
): Promise<void> {
	await exec.query(
		`UPDATE work_instruction SET
		   routing = COALESCE(routing, $2),
		   qty = COALESCE(NULLIF(qty, ''), $3),
		   item_id = COALESCE(item_id, (SELECT id FROM item_info WHERE item_number = $4 AND company_code = $5 LIMIT 1)),
		   updated_date = NOW()
		 WHERE id = $1 AND company_code = $5
		   AND (routing IS NULL OR qty IS NULL OR qty = '' OR item_id IS NULL)`,
		[wiId, routing, qty != null ? String(qty) : null, itemNumber, companyCode],
	);
}

/**
 * 체크리스트 복사 공통 함수
 * 접수 카드(work_order_process_result) 생성 시 마스터 공정의 체크리스트 템플릿을
 * 새 접수 row에 복사한다.
 *
 * 주의: process_work_result.work_order_process_id 컬럼은
 *   - 마스터 체크리스트(routing 스냅샷): work_order_process.id 저장 (createWorkProcesses에서 1회)
 *   - 접수 카드 체크리스트: work_order_process_result.id 저장 (acceptProcess 호출 시)
 * 컬럼명은 DB 스키마상 변경 금지.
 *
 * 전략: routingDetailId가 있으면 원본 템플릿에서, 없으면 마스터의 기존 체크리스트에서 복사
 *
 * options:
 *   - workInstructionNo: 주어지고 wi_process_work_item 에 (wiNo, routingDetailId) row 가 있으면
 *     원본(process_work_item) 대신 wi_* 템플릿에서 복사 (작업지시별 커스텀 우선)
 *   - skipAStrategy: true 면 A 전략(템플릿 복사)을 건너뛰고 바로 B 전략(마스터 스냅샷 복사)으로 진입
 *     (접수 시 마스터 스냅샷 일관성 보장용)
 */
export async function copyChecklistToSplit(
	client: { query: (text: string, values?: any[]) => Promise<any> },
	masterProcessId: string,
	wopResultId: string,
	routingDetailId: string | null,
	companyCode: string,
	userId: string,
	options?: { workInstructionNo?: string; skipAStrategy?: boolean },
): Promise<number> {
	// A. routing_detail_id가 있으면 원본 템플릿(process_work_item + detail)에서 복사
	//    단, options.skipAStrategy === true 이면 A 전략 전체를 건너뛰고 B 로 진입
	if (routingDetailId && options?.skipAStrategy !== true) {
		const wiNo = options?.workInstructionNo;
		let wiExists = false;
		if (wiNo) {
			const wiCheck = await client.query(
				`SELECT 1 FROM wi_process_work_item
         WHERE work_instruction_no = $1 AND routing_detail_id = $2 AND company_code = $3
         LIMIT 1`,
				[wiNo, routingDetailId, companyCode],
			);
			wiExists = (wiCheck.rowCount ?? 0) > 0;
		}

		let countA = 0;
		if (wiNo && wiExists) {
			// A-1. wi_* (작업지시 커스텀 템플릿) 에서 복사
			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, wi.company_code, $1,
          wi.id, wid.id,
          wi.work_phase, wi.title, wi.sort_order::text,
          wid.content, wid.detail_type, wid.sort_order::text, wid.is_required,
          wid.inspection_code, wid.inspection_method, wid.unit, wid.lower_limit, wid.upper_limit,
          wid.input_type, wid.lookup_target, wid.display_fields, wid.duration_minutes::text,
          'pending', $2
        FROM wi_process_work_item wi
        JOIN wi_process_work_item_detail wid
          ON wid.wi_work_item_id = wi.id AND wid.company_code = wi.company_code
        WHERE wi.work_instruction_no = $5
          AND wi.routing_detail_id = $3
          AND wi.company_code = $4
        ORDER BY wi.sort_order::int, wid.sort_order::int`,
				[wopResultId, userId, routingDetailId, companyCode, wiNo],
			);
			countA = result.rowCount ?? 0;
		} else {
			// A-2. 원본 템플릿(process_work_item) 에서 복사 (기존 동작)
			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`,
				[wopResultId, userId, routingDetailId, companyCode],
			);
			countA = result.rowCount ?? 0;
		}
		if (countA > 0) return countA;
		// A 전략에서 0건이면 B 전략(마스터 wop의 체크리스트 복사)으로 fallthrough
	}

	// B. routing_detail_id가 없거나 A 전략에서 0건이면 마스터 wop의 process_work_result 구조 복사
	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, 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`,
		[wopResultId, userId, masterProcessId, companyCode],
	);
	return result.rowCount ?? 0;
}

/**
 * 내부 헬퍼: 단일 작업지시에 대해 work_order_process(마스터) + process_work_result(체크리스트 스냅샷) 생성
 */
async function generateWorkProcessesForInstruction(
	client: { query: (text: string, values?: any[]) => Promise<any> },
	workInstructionId: string,
	routingVersionId: string,
	planQty: string | null,
	companyCode: string,
	userId: string,
	batchId?: string | null,
): Promise<{
	processes: Array<{
		id: string;
		seq_no: string;
		process_name: string;
		checklist_count: number;
	}>;
	total_checklists: number;
} | null> {
	await client.query(
		`SELECT pg_advisory_xact_lock(hashtext($1))`,
		[`wop_sync:${companyCode}:${workInstructionId}:${batchId || ""}`],
	);

	if (batchId) {
		const existCheck = await client.query(
			`SELECT COUNT(*) as cnt FROM work_order_process
       WHERE wo_id = $1 AND company_code = $2
         AND (batch_id = $3 OR batch_id IS NULL)`,
			[workInstructionId, companyCode, batchId],
		);
		if (parseInt(existCheck.rows[0].cnt, 10) > 0) {
			return null;
		}
	} else {
		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;
		}
	}

	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;
	}

	// 작업지시번호 1회 조회 — copyChecklistToSplit 에 전달하여 wi_* 커스텀 템플릿 우선 적용
	const wiRow = await client.query(
		`SELECT work_instruction_no FROM work_instruction WHERE id = $1 AND company_code = $2`,
		[workInstructionId, companyCode],
	);
	const workInstructionNo = wiRow.rows[0]?.work_instruction_no as string | undefined;

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

	for (const rd of routingDetails.rows) {
		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,
        routing_detail_id, batch_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,
				rd.id,
				batchId || null,
				userId,
			],
		);
		const wopId = wopResult.rows[0].id;

		const checklistCount = await copyChecklistToSplit(
			client,
			wopId,
			wopId,
			rd.id,
			companyCode,
			userId,
			{ workInstructionNo },
		);
		totalChecklists += checklistCount;

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

	return { processes, total_checklists: totalChecklists };
}

/**
 * 작업지시 공정 일괄 생성
 */
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는 필수입니다.",
			});
		}

		await client.query("BEGIN");

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

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

		await client.query("COMMIT");

		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
 */
export const syncWorkInstructions = async (
	req: AuthenticatedRequest,
	res: Response,
) => {
	const pool = getPool();

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

		await ensureBatchIdColumn();

		const unsyncedResult = await pool.query(
			`SELECT wi.id, wi.work_instruction_no,
			        wi.routing AS header_routing,
			        wi.qty AS header_qty,
			        wi.item_id AS header_item_id
       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
           ))
           OR
           EXISTS (
             SELECT 1 FROM work_instruction_detail wid
             WHERE wid.work_instruction_id = wi.id
               AND wid.routing_version_id IS NOT NULL
               AND CAST(COALESCE(NULLIF(wid.qty, ''), '0') AS numeric) > 0
               AND NOT EXISTS (
                 SELECT 1 FROM work_order_process wop
                 WHERE wop.wo_id = wi.id AND wop.company_code = $1
                   AND wop.batch_id = wid.item_number
               )
           )
         )`,
			[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;
			item_number?: string;
			status: "synced" | "skipped" | "error";
			process_count?: number;
			error?: string;
		}> = [];

		for (const wi of unsynced) {
			const detailResult = await pool.query(
				`SELECT wid.item_number, wid.routing_version_id, wid.qty
         FROM work_instruction_detail wid
         WHERE wid.work_instruction_id = $1
           AND wid.routing_version_id IS NOT NULL
           AND CAST(COALESCE(NULLIF(wid.qty, ''), '0') AS numeric) > 0
         ORDER BY wid.created_date ASC`,
				[wi.id],
			);

			const detailRows = detailResult.rows;

			if (detailRows.length === 0 && wi.header_routing) {
				const firstDetail = await pool.query(
					`SELECT routing_version_id, qty, item_number
           FROM work_instruction_detail
           WHERE work_instruction_id = $1
           LIMIT 1`,
					[wi.id],
				);
				const wid = firstDetail.rows[0];
				if (wid) {
					await updateWorkInstructionHeader(
						pool,
						wi.id,
						companyCode,
						wid.routing_version_id,
						wid.qty,
						wid.item_number,
					);
				}

				const client = await pool.connect();
				try {
					await client.query("BEGIN");
					const result = await generateWorkProcessesForInstruction(
						client,
						wi.id,
						wi.header_routing,
						wi.header_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",
						});
					} else {
						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,
						});
					}
				} 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 || "알 수 없는 오류",
					});
				} finally {
					client.release();
				}
				continue;
			}

			if (detailRows.length > 0) {
				const first = detailRows[0];
				await updateWorkInstructionHeader(
					pool,
					wi.id,
					companyCode,
					first.routing_version_id,
					first.qty,
					first.item_number,
				);
			}

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

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

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

					await client.query("COMMIT");
					synced++;
					details.push({
						work_instruction_id: wi.id,
						work_instruction_no: wi.work_instruction_no,
						item_number: detail.item_number,
						status: "synced",
						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,
						item_number: detail.item_number,
						status: "error",
						error: err.message || "알 수 없는 오류",
					});
				} finally {
					client.release();
				}
			}
		}

		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 || "작업지시 동기화 중 오류가 발생했습니다.",
		});
	}
};

/**
 * 타이머 API — work_order_process_result 기준.
 * 요청 본문 work_order_process_id 는 work_order_process_result.id
 */
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 중 하나여야 합니다.",
			});
		}

		let result;

		switch (action) {
			case "start":
				result = await pool.query(
					`UPDATE work_order_process_result
           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_result
           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":
				result = await pool.query(
					`UPDATE work_order_process_result
           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_result
           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: "대상 접수 카드를 찾을 수 없거나 현재 상태에서 수행할 수 없습니다.",
			});
		}

		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 || "타이머 처리 중 오류가 발생했습니다.",
		});
	}
};

/**
 * Phase/그룹 타이머 제어
 * process_work_result.group_* 컬럼을 phase 단위로 공유.
 * complete 액션에서 wop_result.actual_work_time 합산 갱신.
 *
 * Body:
 *  - phase (optional): 있으면 해당 phase 내 모든 row 를 동일하게 업데이트
 *  - item_id (optional): phase 가 없을 때만 단일 row 대상 (backward-compat)
 *  - work_order_process_id (required with phase, optional with item_id)
 *  - action: start / pause / resume / complete
 */
export const controlGroupTimer = async (
	req: AuthenticatedRequest,
	res: Response,
) => {
	const pool = getPool();

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

		if (!action) {
			return res.status(400).json({
				success: false,
				message: "action은 필수입니다.",
			});
		}
		if (!phase && !item_id) {
			return res.status(400).json({
				success: false,
				message: "phase 또는 item_id 중 하나는 필수입니다.",
			});
		}
		if (phase && !work_order_process_id) {
			return res.status(400).json({
				success: false,
				message: "phase 모드에서는 work_order_process_id 가 필수입니다.",
			});
		}

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

		// phase 모드: phase 내 모든 row 에 동일값 반영 / fallback: item_id 단일 row
		const whereClause = phase
			? `work_order_process_id = $1 AND work_phase = $2 AND company_code = $3`
			: `id = $1 AND company_code = $2`;
		const baseParams: unknown[] = phase
			? [work_order_process_id, phase, companyCode]
			: [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,
				);
				if (work_order_process_id) {
					await pool.query(
						`UPDATE work_order_process_result
           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,
				);

				// 접수 카드(wop_result) 의 actual_work_time 합산 갱신
				// phase 내 모든 row 가 동일값을 가지므로 phase 별 MAX 로 대표값 추출 후 SUM
				if (work_order_process_id) {
					await pool.query(
						`UPDATE work_order_process_result wr
             SET actual_work_time = sub.total_work_seconds::text,
                 updated_date = NOW()
             FROM (
               SELECT COALESCE(SUM(phase_seconds), 0) AS total_work_seconds
               FROM (
                 SELECT work_phase,
                   MAX(
                     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
                   ) AS phase_seconds
                 FROM process_work_result
                 WHERE work_order_process_id = $1 AND company_code = $2
                 GROUP BY work_phase
               ) p
             ) sub
             WHERE wr.id = $1 AND wr.company_code = $2`,
						[work_order_process_id, companyCode],
					);
				}
				break;
			}
		}

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

		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);

		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 || "불량 유형 조회 중 오류가 발생했습니다.",
		});
	}
};

/**
 * 실적 저장 (누적 방식) — work_order_process_result UPDATE
 * work_order_process_id 는 work_order_process_result.id
 */
export const saveResult = async (req: AuthenticatedRequest, res: Response) => {
	const pool = getPool();

	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;

	// validation: BEGIN 이전에 처리
	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 wr.status, wr.result_status, wr.total_production_qty, wr.good_qty,
              wr.defect_qty, wr.concession_qty, wr.defect_detail,
              wr.input_qty, wr.wop_id, wop.wo_id, wop.seq_no
       FROM work_order_process_result wr
       JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
       WHERE wr.id = $1 AND wr.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.result_status === "confirmed") {
		return res.status(403).json({
			success: false,
			message: "이미 확정된 실적입니다. 추가 등록이 불가능합니다.",
		});
	}

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

		// 초과 생산 경고 (차단하지 않음)
		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,
			});
		}

		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 {
				/* ignore */
			}
			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 client.query(
			`UPDATE work_order_process_result
       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) {
			await client.query("ROLLBACK");
			return res.status(404).json({
				success: false,
				message: "접수 카드를 찾을 수 없거나 권한이 없습니다.",
			});
		}

		// 현재 접수 카드 공정 정보
		const currentSeq = await client.query(
			`SELECT wr.id as wr_id, wr.wop_id, wr.input_qty as current_input_qty,
              wop.seq_no, wop.wo_id,
              wop.process_code, wop.process_name,
              wop.is_required, wop.is_fixed_order, wop.standard_time,
              wr.equipment_code, wop.routing_detail_id,
              wi.qty as instruction_qty
       FROM work_order_process_result wr
       JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
       JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code
       WHERE wr.id = $1 AND wr.company_code = $2`,
			[work_order_process_id, companyCode],
		);

		// 재작업 카드 자동 생성 (disposition = 'rework' 항목이 있을 때)
		// 신 구조: work_order_process_result 에 새 row INSERT (is_rework='Y', seq=MAX+1, UNIQUE 충돌 시 1회 재시도)
		if (
			(currentSeq.rowCount ?? 0) > 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];
				let reworkMasterId: string = proc.wop_id;
				if (targetProcessCode) {
					const targetProc = await client.query(
						`SELECT id FROM work_order_process
             WHERE wo_id = $1 AND process_code = $2 AND company_code = $3
             LIMIT 1`,
						[proc.wo_id, targetProcessCode, companyCode],
					);
					if ((targetProc.rowCount ?? 0) > 0) {
						reworkMasterId = targetProc.rows[0].id;
					}
				}

				let reworkId: string | null = null;
				for (let attempt = 0; attempt < 2 && !reworkId; attempt++) {
					try {
						const reworkInsert = await client.query(
							`INSERT INTO work_order_process_result (
                id, wop_id, seq, company_code,
                status, result_status,
                input_qty, good_qty, defect_qty, concession_qty, total_production_qty,
                is_rework, rework_source_id,
                writer, created_date, updated_date
              ) VALUES (
                gen_random_uuid()::text, $1,
                (SELECT COALESCE(MAX(seq), 0) + 1 FROM work_order_process_result WHERE wop_id = $1 AND company_code = $2),
                $2,
                'acceptable', 'draft',
                $3, '0', '0', '0', '0',
                'Y', $4,
                $5, NOW(), NOW()
              ) RETURNING id`,
							[
								reworkMasterId,
								companyCode,
								String(totalReworkQty),
								work_order_process_id,
								userId,
							],
						);
						reworkId = reworkInsert.rows[0]?.id;
					} catch (err: any) {
						if (err?.code !== "23505" || attempt >= 1) throw err;
					}
				}

				if (reworkId) {
					await copyChecklistToSplit(
						client,
						reworkMasterId,
						reworkId,
						null,
						companyCode,
						userId,
					);
				}
			}
		}

		// 접수 카드 자동완료 판정 + 작업지시 완료 캐스케이드
		if ((currentSeq.rowCount ?? 0) > 0) {
			const {
				wo_id: csWoId,
				current_input_qty: csInputQty,
			} = currentSeq.rows[0];
			const csMyInput = parseInt(csInputQty, 10) || 0;

			if (newTotal >= csMyInput && csMyInput > 0) {
				await client.query(
					`UPDATE work_order_process_result
           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],
				);
			}

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

		const latestData = await client.query(
			`SELECT id, total_production_qty, good_qty, defect_qty, concession_qty, defect_detail,
              result_note, result_status, status, input_qty,
              is_rework, rework_source_id
       FROM work_order_process_result WHERE id = $1 AND company_code = $2`,
			[work_order_process_id, companyCode],
		);

		await client.query("COMMIT");

		const responseData = latestData.rows[0] || result.rows[0];

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

/**
 * 작업지시(work_instruction) 전체 완료 판정
 * 신 구조: 마지막 seq 의 각 마스터 wop 에 대해 그 wop 의 wop_result 들이
 * 모두 result_status='confirmed' 이고 SUM(good+concession) >= wi.qty 이면 완료 처리.
 */
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);

	// 마지막 seq 의 마스터 wop 중 confirmed 가 하나라도 없는 게 있으면 미완료
	const incompleteCheck = await pool.query(
		`SELECT wop.id
     FROM work_order_process wop
     WHERE wop.wo_id = $1 AND wop.seq_no = $2 AND wop.company_code = $3
       AND NOT EXISTS (
         SELECT 1 FROM work_order_process_result wr
         WHERE wr.wop_id = wop.id AND wr.company_code = wop.company_code
           AND wr.result_status = 'confirmed'
       )
     LIMIT 1`,
		[woId, maxSeq, companyCode],
	);
	if ((incompleteCheck.rowCount ?? 0) > 0) return;

	const totalGoodResult = await pool.query(
		`SELECT COALESCE(SUM(CAST(NULLIF(wr.good_qty, '') AS int)), 0)
            + COALESCE(SUM(CAST(NULLIF(wr.concession_qty, '') AS int)), 0) as total_good
     FROM work_order_process_result wr
     JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
     WHERE wop.wo_id = $1 AND wop.seq_no = $2 AND wop.company_code = $3`,
		[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],
	);

	if ((updateResult.rowCount ?? 0) > 0 && completedQty > 0) {
		try {
			const itemId = updateResult.rows[0].item_id;

			const itemResult = await pool.query(
				`SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`,
				[itemId, companyCode],
			);
			if (itemResult.rowCount === 0) 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
         LIMIT 1`,
				[woId, maxSeq, companyCode],
			);

			if (
				warehouseResult.rowCount === 0 ||
				!warehouseResult.rows[0].target_warehouse_id
			) {
				return;
			}

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

			await upsertInventoryStock(
				pool,
				companyCode,
				itemCode,
				warehouseCode,
				locationCode,
				completedQty,
				userId,
			);
		} catch (inventoryError: any) {
			logger.error(
				"[pop/production] 재고입고 오류 (공정 완료는 유지):",
				inventoryError,
			);
		}
	}
};

/**
 * 실적 확정 — work_order_process_result UPDATE.
 * work_order_process_id 는 work_order_process_result.id
 */
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_result
       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: "등록된 실적이 없습니다. 실적을 먼저 등록해주세요.",
			});
		}

		const result = await pool.query(
			`UPDATE work_order_process_result
       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, wop_id`,
			[work_order_process_id, companyCode, userId],
		);

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

		// 작업지시 완료 캐스케이드
		const wopLookup = await pool.query(
			`SELECT wo_id FROM work_order_process WHERE id = $1 AND company_code = $2`,
			[result.rows[0].wop_id, companyCode],
		);
		if ((wopLookup.rowCount ?? 0) > 0) {
			await checkAndCompleteWorkInstruction(
				pool,
				wopLookup.rows[0].wo_id,
				companyCode,
				userId,
			);
		}

		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_result 기준 (wop_result.id)
 */
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_result WHERE id = $1 AND company_code = $2`,
			[work_order_process_id, companyCode],
		);
		if (ownerCheck.rowCount === 0) {
			return res
				.status(404)
				.json({ success: false, message: "접수 카드를 찾을 수 없습니다." });
		}

		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,
			};
		});

		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 || "이력 조회 중 오류가 발생했습니다.",
		});
	}
};

/**
 * 앞공정 완료량 + 접수가능량 조회 (신 구조)
 * work_order_process_id 는 마스터 work_order_process.id
 */
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.id, wop.seq_no, wop.wo_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);

		// 기접수합계 (같은 wop_id, 리워크 제외)
		const totalAccepted = await pool.query(
			`SELECT COALESCE(SUM(GREATEST(
          CAST(COALESCE(NULLIF(input_qty, ''), '0') AS int),
          CAST(COALESCE(NULLIF(total_production_qty, ''), '0') AS int)
        )), 0) as total_input
       FROM work_order_process_result
       WHERE wop_id = $1 AND company_code = $2
         AND (is_rework IS NULL OR is_rework NOT IN ('Y','true','1'))`,
			[work_order_process_id, companyCode],
		);
		const myInputQty = parseInt(totalAccepted.rows[0].total_input, 10) || 0;

		const prevGoodResult = await getPrevProcessGoodQty(pool, wo_id, seqNum, companyCode);
		const prevGoodQty = prevGoodResult !== null ? prevGoodResult : instrQty;

		const availableQty = Math.max(0, prevGoodQty - myInputQty);

		// 앞공정 리워크 미소진
		let reworkAvailableQty = 0;
		if (seqNum > 1) {
			const prevSeq = String(seqNum - 1);
			const reworkSplits = await pool.query(
				`SELECT wr.rework_source_id, COALESCE(SUM(CAST(NULLIF(wr.good_qty, '') AS int)), 0) as rg
         FROM work_order_process_result wr
         JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
         WHERE wop.wo_id = $1 AND wop.seq_no = $2 AND wop.company_code = $3
           AND wr.is_rework IN ('Y','true','1')
           AND wr.status = 'completed'
           AND CAST(NULLIF(wr.good_qty, '') AS int) > 0
         GROUP BY wr.rework_source_id`,
				[wo_id, prevSeq, companyCode],
			);
			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(CAST(NULLIF(wr.input_qty, '') AS int)), 0) as consumed
           FROM work_order_process_result wr
           JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
           WHERE wop.wo_id = $1 AND wop.seq_no = $2 AND wop.company_code = $3
             AND wr.is_rework IN ('Y','true','1')
             AND wr.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 || "접수가능량 조회 중 오류가 발생했습니다.",
		});
	}
};

/**
 * 공정 접수 — work_order_process_result INSERT (신 구조)
 * body: { work_order_process_id, accept_qty, batch_id?, rework_source_id?, equipment_code? }
 * work_order_process_id 는 항상 마스터 work_order_process.id
 */
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) {
			return res.status(400).json({
				success: false,
				message: "work_order_process_id와 accept_qty가 필요합니다.",
			});
		}

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

		await client.query("BEGIN");

		// wi_* 편집(syncMasterChecklistFromWi) 과 직렬화하여 마스터 스냅샷 불변 계약 보장
		const woRow = await client.query(
			`SELECT wo_id FROM work_order_process WHERE id = $1 AND company_code = $2`,
			[work_order_process_id, companyCode],
		);
		if (woRow.rowCount === 0) {
			await client.query("ROLLBACK");
			return res
				.status(404)
				.json({ success: false, message: "공정을 찾을 수 없습니다." });
		}
		await client.query(
			`SELECT pg_advisory_xact_lock(hashtext($1))`,
			[`wi_snapshot:${companyCode}:${woRow.rows[0].wo_id}`],
		);

		// 마스터 wop FOR UPDATE (동시 접수 race 방지)
		const current = await client.query(
			`SELECT wop.id, wop.seq_no, wop.wo_id,
              wop.process_code, wop.process_name, wop.is_required, wop.is_fixed_order,
              wop.standard_time, 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");
			return res
				.status(404)
				.json({ success: false, message: "공정을 찾을 수 없습니다." });
		}

		const row = current.rows[0];
		const masterId: string = row.id;
		const instrQty = parseInt(row.instruction_qty, 10) || 0;
		const seqNum = parseInt(row.seq_no, 10);

		// 완료 판정: confirmed 존재 + SUM(good+concession) >= instrQty → 거부
		const completedCheck = await client.query(
			`SELECT
         EXISTS (
           SELECT 1 FROM work_order_process_result
           WHERE wop_id = $1 AND company_code = $2 AND result_status = 'confirmed'
         ) AS has_confirmed,
         COALESCE(SUM(CAST(NULLIF(good_qty, '') AS int)), 0)
           + COALESCE(SUM(CAST(NULLIF(concession_qty, '') AS int)), 0) AS total_good
       FROM work_order_process_result
       WHERE wop_id = $1 AND company_code = $2`,
			[masterId, companyCode],
		);
		const hasConfirmed = completedCheck.rows[0].has_confirmed === true;
		const totalGood = parseInt(completedCheck.rows[0].total_good, 10) || 0;
		if (hasConfirmed && instrQty > 0 && totalGood >= instrQty) {
			await client.query("ROLLBACK");
			return res
				.status(400)
				.json({ success: false, message: "이미 완료된 공정입니다." });
		}

		// 기접수합계 (리워크 제외)
		const totalAccepted = await client.query(
			`SELECT COALESCE(SUM(GREATEST(
          CAST(COALESCE(NULLIF(input_qty, ''), '0') AS int),
          CAST(COALESCE(NULLIF(total_production_qty, ''), '0') AS int)
        )), 0) as total_input
       FROM work_order_process_result
       WHERE wop_id = $1 AND company_code = $2
         AND (is_rework IS NULL OR is_rework != 'Y')`,
			[masterId, companyCode],
		);
		const currentTotalInput =
			parseInt(totalAccepted.rows[0].total_input, 10) || 0;

		// 앞공정 양품 계산
		const prevGoodResult = await getPrevProcessGoodQty(client, row.wo_id, seqNum, companyCode);
		const prevGoodQty = prevGoodResult !== null ? prevGoodResult : instrQty;

		const availableQty = prevGoodQty - currentTotalInput;

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

		// 리워크 정보 결정
		let splitIsRework: string | null = null;
		let splitReworkSourceId: string | null = null;

		if (req.body.rework_source_id) {
			splitIsRework = "Y";
			splitReworkSourceId = req.body.rework_source_id;
		} else if (seqNum > 1) {
			const prevSeq = String(seqNum - 1);
			const prevReworkSplits = await client.query(
				`SELECT wr.rework_source_id,
                COALESCE(SUM(CAST(NULLIF(wr.good_qty, '') AS int)), 0) as rework_good
         FROM work_order_process_result wr
         JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
         WHERE wop.wo_id = $1 AND wop.seq_no = $2 AND wop.company_code = $3
           AND wr.is_rework = 'Y'
           AND wr.status = 'completed'
           AND CAST(NULLIF(wr.good_qty, '') AS int) > 0
         GROUP BY wr.rework_source_id`,
				[row.wo_id, prevSeq, companyCode],
			);
			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(CAST(NULLIF(wr.input_qty, '') AS int)), 0) as consumed
           FROM work_order_process_result wr
           JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
           WHERE wop.wo_id = $1 AND wop.seq_no = $2 AND wop.company_code = $3
             AND wr.is_rework = 'Y'
             AND wr.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;
					}
					break;
				}
			}
		}

		// work_order_process_result INSERT (seq=MAX+1, UNIQUE 충돌 시 1회 재시도)
		let inserted: any = null;
		for (let attempt = 0; attempt < 2 && !inserted; attempt++) {
			try {
				const result = await client.query(
					`INSERT INTO work_order_process_result (
            id, wop_id, seq, company_code,
            status, result_status,
            accepted_by, accepted_at, started_at,
            input_qty, good_qty, defect_qty, concession_qty, total_production_qty,
            equipment_code, batch_id,
            is_rework, rework_source_id,
            writer, created_date, updated_date
          ) VALUES (
            gen_random_uuid()::text, $1::varchar,
            (SELECT COALESCE(MAX(seq), 0) + 1 FROM work_order_process_result WHERE wop_id = $1 AND company_code = $2),
            $2,
            'in_progress', 'draft',
            $3, NOW()::text, NOW()::text,
            $4, '0', '0', '0', '0',
            $5, $6,
            $7, $8,
            $3, NOW(), NOW()
          ) RETURNING id, seq, input_qty, status, result_status, accepted_by`,
					[
						masterId,
						companyCode,
						userId,
						String(qty),
						req.body.equipment_code || null,
						req.body.batch_id || null,
						splitIsRework,
						splitReworkSourceId,
					],
				);
				inserted = result.rows[0];
			} catch (err: any) {
				if (err?.code !== "23505" || attempt >= 1) {
					await client.query("ROLLBACK");
					logger.error("[pop/production] accept-process INSERT 오류:", err);
					return res.status(500).json({ success: false, message: err.message });
				}
			}
		}

		// 체크리스트 복사: masterId → wop_result.id
		// 접수는 항상 마스터 스냅샷(B 전략)에서만 복사 — 첫 접수 이후 일관성 보장
		const checklistCount = await copyChecklistToSplit(
			client,
			masterId,
			inserted.id,
			row.routing_detail_id,
			companyCode,
			userId,
			{ skipAStrategy: true },
		);

		await client.query("COMMIT");

		logger.info("[pop/production] accept-process 접수 완료", {
			companyCode,
			userId,
			masterId,
			wopResultId: inserted.id,
			seq: inserted.seq,
			acceptedQty: qty,
			checklistCount,
		});

		const acceptData: any = {
			...inserted,
			process_name: row.process_name,
		};
		if (splitReworkSourceId) {
			acceptData.rework_source_id = splitReworkSourceId;
			acceptData.is_rework = splitIsRework;
		}

		return res.json({
			success: true,
			data: acceptData,
			message: `${qty}개 접수 완료`,
		});
	} 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();
	}
};

/**
 * 접수 취소 — work_order_process_result 기준
 * work_order_process_id 는 wop_result.id
 */
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 wr.id, wr.status, wr.input_qty, wr.total_production_qty, wr.result_status,
              wr.wop_id, wr.good_qty, wr.concession_qty,
              wop.wo_id, wop.seq_no, wop.process_name,
              wop.target_warehouse_id, wop.target_location_code
       FROM work_order_process_result wr
       JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
       WHERE wr.id = $1 AND wr.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.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:
					"취소할 미소진 접수분이 없습니다. 모든 접수량에 대해 실적이 등록되었습니다.",
			});
		}

		const 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_result WHERE id = $1 AND company_code = $2`,
					[work_order_process_id, companyCode],
				);
			} else {
				await client.query(
					`UPDATE work_order_process_result
           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],
				);
			}

			// 재고 원복
			if (proc.target_warehouse_id) {
				const inboundQty =
					parseInt(proc.good_qty || "0", 10) +
					parseInt(proc.concession_qty || "0", 10);
				if (inboundQty > 0) {
					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) > 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) > 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,
								],
							);
						}
					}
				}
			}

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

		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 });
	}
};

/**
 * 특정 창고의 위치(로케이션) 목록 조회
 */
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는 필수입니다." });
		}

		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 });
	}
};

/**
 * 마지막 공정 여부 확인 — 마스터 wop 기준 (seq_no MAX)
 */
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 } });
		}

		const process = await pool.query(
			`SELECT wo_id, seq_no, target_warehouse_id, target_location_code
       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, target_warehouse_id, target_location_code } = process.rows[0];

		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)
       LIMIT 1`,
			[wo_id, companyCode, seq_no],
		);

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

/**
 * 공정의 목표 창고/위치 업데이트 (마스터 wop 기준)
 */
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는 필수입니다.",
			});
		}

		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`,
			[
				work_order_process_id,
				companyCode,
				target_warehouse_id,
				target_location_code || null,
				userId,
			],
		);

		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 });
	}
};

/**
 * 독립 재고 입고 — work_order_process_result 기준.
 * body.work_order_process_id 는 wop_result.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");

		const procResult = await client.query(
			`SELECT wr.wop_id, wr.good_qty, wr.concession_qty, wr.is_rework,
              wop.wo_id, wop.seq_no, wop.target_warehouse_id
       FROM work_order_process_result wr
       JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
       WHERE wr.id = $1 AND wr.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];

		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이므로 재고 입고할 수 없습니다.",
			});
		}

		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;

		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;

		await upsertInventoryStock(
			client,
			companyCode,
			itemCode,
			warehouse_code,
			effectiveLocationCode,
			goodQty,
			userId,
		);

		// 마스터 wop 에 target_warehouse_id 저장
		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`,
			[proc.wop_id, companyCode, warehouse_code, location_code || null, userId],
		);

		if (proc.is_rework === "Y") {
			await client.query(
				`UPDATE work_order_process_result SET is_rework = NULL, updated_date = NOW()
         WHERE id = $1 AND company_code = $2`,
				[work_order_process_id, companyCode],
			);
		}

		await client.query("COMMIT");

		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();
	}
};

/**
 * 간이 재고 입고 (공정 접수 없이 바로 입고)
 */
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");

		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;

		await upsertInventoryStock(
			client,
			companyCode,
			itemCode,
			warehouse_code,
			effectiveLocationCode,
			parsedQty,
			userId,
		);

		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");

		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();
	}
};

/**
 * 재작업 이력 조회 — 신 구조: 접수 카드(wop_result) 기반
 */
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 wr.id, wop.seq_no, wop.process_code, wop.process_name,
              wr.status, wr.input_qty, wr.good_qty, wr.defect_qty, wr.concession_qty,
              wr.is_rework, wr.rework_source_id, wr.wop_id,
              wr.accepted_by, wr.accepted_at, wr.started_at, wr.completed_at,
              wr.created_date
       FROM work_order_process_result wr
       JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
       WHERE wop.wo_id = $1 AND wr.company_code = $2
         AND (wr.is_rework = 'Y' OR wr.is_rework = '1' OR CAST(NULLIF(wr.defect_qty, '') AS int) > 0)
       ORDER BY wr.created_date ASC`,
			[woId, companyCode],
		);

		const rows = result.rows;
		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 자재 목록 + 소요량 계산
 * processId = 마스터 work_order_process.id
 */
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 필수" });
		}

		const procResult = await pool.query(
			`SELECT wop.wo_id, wop.process_code, wop.plan_qty,
              wi.item_id, wi.qty as instruction_qty,
              (SELECT COALESCE(SUM(CAST(NULLIF(input_qty, '') AS int)), 0)
                 FROM work_order_process_result
                 WHERE wop_id = wop.id AND company_code = wop.company_code) as total_input
       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(
			String(
				proc.total_input && parseInt(proc.total_input, 10) > 0
					? proc.total_input
					: proc.plan_qty || proc.instruction_qty || "0",
			),
			10,
		);

		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;

		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,
              b.base_qty
       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],
		);

		const baseQty = parseFloat(bomResult.rows[0]?.base_qty || "1") || 1;

		// 투입량: 마스터 wop.id 직결 + 접수 카드들(wop_result.id) 의 material_input 전부 합산
		const inputResult = await pool.query(
			`SELECT detail_content as item_code, SUM(CAST(NULLIF(result_value, '') AS numeric)) as total_input
       FROM process_work_result
       WHERE company_code = $1
         AND detail_type = 'material_input'
         AND result_value IS NOT NULL AND result_value != ''
         AND (
           work_order_process_id = $2
           OR work_order_process_id IN (
             SELECT id FROM work_order_process_result WHERE wop_id = $2 AND company_code = $1
           )
         )
       GROUP BY detail_content`,
			[companyCode, processId],
		);
		const inputMap = new Map<string, number>();
		for (const row of inputResult.rows) {
			inputMap.set(String(row.item_code), 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 childItemCode = String(bd.child_item_code || "");
			return {
				id: bd.id,
				child_item_id: bd.child_item_id,
				child_item_code: childItemCode,
				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(childItemCode) || 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 });
	}
};

/**
 * 자재 투입 기록 저장
 * work_order_process_id 는 wop_result.id (접수 카드) 기준으로 저장.
 */
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;
			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;

			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,
				],
			);

			let effectiveWh = warehouse_code;
			let effectiveLoc = location_code;
			if (!effectiveWh) {
				const autoStock = await client.query(
					`SELECT warehouse_code, location_code FROM inventory_stock
					 WHERE company_code = $1 AND item_code = $2
					   AND COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) > 0
					 ORDER BY last_in_date DESC NULLS LAST LIMIT 1`,
					[companyCode, effectiveItemCode],
				);
				if (autoStock.rows.length > 0) {
					effectiveWh = autoStock.rows[0].warehouse_code;
					effectiveLoc = autoStock.rows[0].location_code || effectiveWh;
				}
			}
			if (effectiveWh) {
				const locCode = effectiveLoc || effectiveWh;
				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,
						effectiveWh,
						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();
	}
};

/**
 * 자재 투입 현황 조회
 * processId 는 wop_result.id (접수 카드) 기준
 */
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 });
	}
};

/**
 * 체크리스트 조회
 * processId 는 wop_result.id (접수 카드) 기준
 */
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 });
	}
};

/* ================================================================== */
/* getProcessList — 신 구조 (work_order_process + work_order_process_result) */
/* ================================================================== */

let _processListIndexesEnsured = false;
async function ensureProcessListIndexes() {
	if (_processListIndexesEnsured) return;
	try {
		const pool = getPool();
		await pool.query(
			"CREATE INDEX IF NOT EXISTS idx_wop_wo_seq ON work_order_process (wo_id, seq_no)",
		);
		_processListIndexesEnsured = true;
	} catch {
		/* ignore */
	}
}

export const getProcessList = async (
	req: AuthenticatedRequest,
	res: Response,
) => {
	const pool = getPool();
	try {
		const companyCode = req.user!.companyCode;
		await ensureBatchIdColumn();
		await ensureProcessListIndexes();

		// 마스터 wop 행 + wop_result 집계 + 앞공정 양품 + 접수 카드 배열
		const sql = `
WITH wop AS (
  SELECT w.*, COALESCE(wi.qty, '0') AS instruction_qty
  FROM work_order_process w
  LEFT JOIN work_instruction wi
    ON w.wo_id = wi.id AND w.company_code = wi.company_code
  WHERE w.company_code = $1
),
first_seq AS (
  SELECT wo_id, MIN(CAST(seq_no AS int)) AS min_seq
  FROM work_order_process
  WHERE company_code = $1
  GROUP BY wo_id
),
wr_agg AS (
  SELECT wop_id,
         COALESCE(SUM(CAST(NULLIF(good_qty, '') AS numeric)), 0) AS sum_good,
         COALESCE(SUM(CAST(NULLIF(defect_qty, '') AS numeric)), 0) AS sum_defect,
         COALESCE(SUM(CAST(NULLIF(concession_qty, '') AS numeric)), 0) AS sum_concession,
         COALESCE(SUM(CAST(NULLIF(total_production_qty, '') AS numeric)), 0) AS sum_total,
         COALESCE(SUM(CAST(NULLIF(input_qty, '') AS numeric)), 0) AS sum_input,
         COALESCE(SUM(GREATEST(
           CAST(COALESCE(NULLIF(input_qty, ''), '0') AS numeric),
           CAST(COALESCE(NULLIF(total_production_qty, ''), '0') AS numeric)
         )) FILTER (WHERE is_rework IS NULL OR is_rework NOT IN ('Y','true','1')), 0) AS sum_input_norework,
         BOOL_OR(result_status = 'confirmed') AS has_confirmed,
         BOOL_AND(status = 'completed') AS all_completed,
         COUNT(*) AS accept_count
  FROM work_order_process_result
  WHERE company_code = $1
  GROUP BY wop_id
),
prev_good AS (
  SELECT wop.wo_id,
         (CAST(wop.seq_no AS int) + 1) AS target_seq,
         COALESCE(SUM(CAST(NULLIF(wr.good_qty, '') AS numeric)), 0)
           + COALESCE(SUM(CAST(NULLIF(wr.concession_qty, '') AS numeric)), 0) AS prev_good_qty
  FROM work_order_process wop
  LEFT JOIN work_order_process_result wr
    ON wr.wop_id = wop.id AND wr.company_code = wop.company_code
  WHERE wop.company_code = $1
  GROUP BY wop.wo_id, wop.seq_no
),
accepted_results AS (
  SELECT wop_id,
         JSON_AGG(
           JSON_BUILD_OBJECT(
             'id', id,
             'seq', seq,
             'status', status,
             'result_status', result_status,
             'input_qty', input_qty,
             'good_qty', good_qty,
             'defect_qty', defect_qty,
             'concession_qty', concession_qty,
             'total_production_qty', total_production_qty,
             'is_rework', is_rework,
             'rework_source_id', rework_source_id,
             'accepted_by', accepted_by,
             'accepted_at', accepted_at,
             'started_at', started_at,
             'completed_at', completed_at,
             'equipment_code', equipment_code,
             'batch_id', batch_id
           ) ORDER BY seq
         ) AS accepted_results
  FROM work_order_process_result
  WHERE company_code = $1
  GROUP BY wop_id
)
SELECT
  wop.*,
  COALESCE(wa.sum_good, 0) AS good_qty,
  COALESCE(wa.sum_defect, 0) AS defect_qty,
  COALESCE(wa.sum_concession, 0) AS concession_qty,
  COALESCE(wa.sum_total, 0) AS total_production_qty,
  COALESCE(wa.sum_input, 0) AS input_qty,
  CASE
    WHEN wa.has_confirmed AND wa.all_completed THEN 'completed'
    WHEN wa.accept_count > 0 AND (
      CASE
        WHEN CAST(wop.seq_no AS int) <= COALESCE(fs.min_seq, 1)
          THEN GREATEST(0,
            COALESCE(CAST(NULLIF(wop.instruction_qty, '') AS numeric), 0)
              - COALESCE(wa.sum_input_norework, 0)
          )
        ELSE GREATEST(0,
          COALESCE(pg.prev_good_qty, 0) - COALESCE(wa.sum_input_norework, 0)
        )
      END
    ) <= 0 THEN 'in_progress'
    WHEN CAST(wop.seq_no AS int) > COALESCE(fs.min_seq, 1)
         AND COALESCE(pg.prev_good_qty, 0) = 0 THEN 'waiting'
    ELSE 'acceptable'
  END AS status,
  CASE WHEN wa.has_confirmed THEN 'confirmed' ELSE 'draft' END AS result_status,
  CASE
    WHEN CAST(wop.seq_no AS int) <= COALESCE(fs.min_seq, 1)
      THEN COALESCE(CAST(NULLIF(wop.instruction_qty, '') AS numeric), 0)
    ELSE COALESCE(pg.prev_good_qty, 0)
  END AS prev_good_qty,
  COALESCE(wa.sum_input_norework, 0) AS my_input_qty,
  CASE
    WHEN CAST(wop.seq_no AS int) <= COALESCE(fs.min_seq, 1)
      THEN GREATEST(0,
        COALESCE(CAST(NULLIF(wop.instruction_qty, '') AS numeric), 0)
          - COALESCE(wa.sum_input_norework, 0)
      )
    ELSE GREATEST(0,
      COALESCE(pg.prev_good_qty, 0) - COALESCE(wa.sum_input_norework, 0)
    )
  END AS available_qty,
  COALESCE(ar.accepted_results, '[]'::json) AS accepted_results
FROM wop
LEFT JOIN wr_agg wa ON wa.wop_id = wop.id
LEFT JOIN first_seq fs ON fs.wo_id = wop.wo_id
LEFT JOIN prev_good pg
  ON pg.wo_id = wop.wo_id
 AND pg.target_seq = CAST(wop.seq_no AS int)
LEFT JOIN accepted_results ar ON ar.wop_id = wop.id
ORDER BY wop.wo_id, CAST(wop.seq_no AS int)
`;

		const result = await pool.query(sql, [companyCode]);
		return res.json({ success: true, data: result.rows });
	} catch (error: any) {
		logger.error("[pop/production] processes 조회 오류:", error);
		return res.status(500).json({ success: false, message: error.message });
	}
};

/**
 * 단일 접수 카드 조회 (신규 엔드포인트)
 * GET /api/pop/production/result/:id
 * :id = work_order_process_result.id
 */
export const getProcessResult = async (
	req: AuthenticatedRequest,
	res: Response,
) => {
	const pool = getPool();
	try {
		const companyCode = req.user!.companyCode;
		const { id } = req.params;
		if (!id) {
			return res
				.status(400)
				.json({ success: false, message: "id는 필수입니다." });
		}

		const result = await pool.query(
			`SELECT wr.*,
              wop.process_code,
              wop.process_name,
              wop.seq_no,
              wop.wo_id,
              wop.routing_detail_id,
              wop.plan_qty,
              wop.target_warehouse_id,
              wop.target_location_code
       FROM work_order_process_result wr
       JOIN work_order_process wop ON wop.id = wr.wop_id AND wop.company_code = wr.company_code
       WHERE wr.id = $1 AND wr.company_code = $2`,
			[id, companyCode],
		);

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

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