<?php

declare(strict_types=1);

namespace App\Libraries;

/**
 * LOT 수불 조회 (레거시 w_gd033a)
 * — 바코드(팩/박스/낱장) 또는 LOT 번호로 일자·입출고처·구분 이력
 */
class BagLotFlowBuilder
{
    private \CodeIgniter\Database\BaseConnection $db;

    public function __construct(?\CodeIgniter\Database\BaseConnection $db = null)
    {
        $this->db = $db ?? \Config\Database::connect();
    }

    /**
     * @return array{
     *   ok: bool,
     *   message: string,
     *   barcode: string,
     *   unit: string,
     *   bag_code: string,
     *   bag_name: string,
     *   lot_no: string,
     *   box_code: string,
     *   pack_code: string,
     *   qty_box: int,
     *   qty_pack: int,
     *   qty_sheet: int,
     *   rows: list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     * }
     */
    public function buildByBarcode(int $lgIdx, string $barcode, bool $queried): array
    {
        $empty = $this->emptyResult($barcode);
        if (! $queried || trim($barcode) === '') {
            return $empty;
        }

        $resolved = $this->resolveBarcode($lgIdx, trim($barcode));
        if (! $resolved['ok']) {
            return array_merge($empty, [
                'message' => (string) ($resolved['message'] ?? '등록되지 않은 바코드입니다.'),
            ]);
        }

        $rows = $this->collectFlowRows($lgIdx, $resolved);
        usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0));

        return array_merge($empty, [
            'ok'        => true,
            'message'   => '',
            'barcode'   => (string) ($resolved['barcode'] ?? $barcode),
            'unit'      => (string) ($resolved['unit'] ?? ''),
            'bag_code'  => (string) ($resolved['bag_code'] ?? ''),
            'bag_name'  => (string) ($resolved['bag_name'] ?? ''),
            'lot_no'    => (string) ($resolved['lot_no'] ?? ''),
            'box_code'  => (string) ($resolved['box_code'] ?? ''),
            'pack_code' => (string) ($resolved['pack_code'] ?? ''),
            'qty_box'   => (int) ($resolved['qty_box'] ?? 0),
            'qty_pack'  => (int) ($resolved['qty_pack'] ?? 0),
            'qty_sheet' => (int) ($resolved['qty_sheet'] ?? 0),
            'rows'      => $rows,
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    public function buildByLotNo(int $lgIdx, string $lotNo, bool $queried): array
    {
        $empty = $this->emptyResult('');
        if (! $queried || trim($lotNo) === '') {
            return $empty;
        }

        $lotNo = trim($lotNo);
        if (! $this->db->tableExists('bag_receiving_pack_code')) {
            return array_merge($empty, ['message' => '바코드(팩) 데이터가 없습니다.']);
        }

        $packRows = $this->db->table('bag_receiving_pack_code')
            ->select('brpc_pack_code, brpc_box_code, brpc_bag_code, brpc_bag_name, brpc_lot_no')
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_lot_no', $lotNo)
            ->limit(500)
            ->get()
            ->getResultArray();

        if ($packRows === []) {
            $order = $this->db->table('bag_order')
                ->where('bo_lg_idx', $lgIdx)
                ->where('bo_lot_no', $lotNo)
                ->orderBy('bo_version', 'DESC')
                ->get()
                ->getRowArray();
            if (! $order) {
                return array_merge($empty, ['message' => '해당 LOT·바코드를 찾을 수 없습니다.', 'lot_no' => $lotNo]);
            }

            return $this->buildLotFromOrderOnly($lgIdx, $lotNo, $order);
        }

        $codes = [];
        $bagCode = '';
        $bagName = '';
        foreach ($packRows as $p) {
            $codes[] = (string) ($p['brpc_pack_code'] ?? '');
            $box = (string) ($p['brpc_box_code'] ?? '');
            if ($box !== '') {
                $codes[] = $box;
            }
            if ($bagCode === '') {
                $bagCode = (string) ($p['brpc_bag_code'] ?? '');
                $bagName = (string) ($p['brpc_bag_name'] ?? '');
            }
        }
        $codes = array_values(array_unique(array_filter($codes, static fn (string $c): bool => $c !== '')));

        $rows = [];
        foreach ($this->loadReceivingEventsForLot($lgIdx, $lotNo) as $ev) {
            $rows[] = $ev;
        }
        foreach ($this->loadScanEventsForCodes($lgIdx, $codes) as $ev) {
            $rows[] = $ev;
        }
        foreach ($this->loadReturnEventsForCodes($lgIdx, $codes) as $ev) {
            $rows[] = $ev;
        }

        usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0));
        if (count($rows) > 500) {
            $rows = array_slice($rows, -500);
        }

        return array_merge($empty, [
            'ok'       => true,
            'lot_no'   => $lotNo,
            'bag_code' => $bagCode,
            'bag_name' => $bagName,
            'barcode'  => $lotNo,
            'rows'     => $rows,
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    private function emptyResult(string $barcode): array
    {
        return [
            'ok'        => false,
            'message'   => '',
            'barcode'   => $barcode,
            'unit'      => '',
            'bag_code'  => '',
            'bag_name'  => '',
            'lot_no'    => '',
            'box_code'  => '',
            'pack_code' => '',
            'qty_box'   => 0,
            'qty_pack'  => 0,
            'qty_sheet' => 0,
            'rows'      => [],
        ];
    }

    /**
     * @return array{ok: bool, message?: string, barcode?: string, unit?: string, bag_code?: string, bag_name?: string, lot_no?: string, box_code?: string, pack_code?: string, pack_ids?: list<int>, qty_box?: int, qty_pack?: int, qty_sheet?: int}
     */
    private function resolveBarcode(int $lgIdx, string $barcode): array
    {
        if (! $this->db->tableExists('bag_receiving_pack_code')) {
            return ['ok' => false, 'message' => '바코드(팩) 데이터가 없습니다.'];
        }

        $pack = $this->db->table('bag_receiving_pack_code')
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_pack_code', $barcode)
            ->get()
            ->getRowArray();
        if ($pack) {
            return $this->resolvedFromPackRow($barcode, '팩', $pack, 0, 1, (int) ($pack['brpc_sheet_qty'] ?? 0));
        }

        $boxRows = $this->db->table('bag_receiving_pack_code')
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_box_code', $barcode)
            ->get()
            ->getResultArray();
        if ($boxRows !== []) {
            $first = $boxRows[0];
            $sheetQty = 0;
            foreach ($boxRows as $row) {
                $sheetQty += (int) ($row['brpc_sheet_qty'] ?? 0);
            }

            return $this->resolvedFromPackRow($barcode, '박스', $first, 1, count($boxRows), $sheetQty);
        }

        $exactSheet = $this->db->table('bag_receiving_pack_code')
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_sheet_start_code', $barcode)
            ->limit(1)
            ->get()
            ->getRowArray();
        if (is_array($exactSheet) && $exactSheet !== []) {
            return $this->resolvedFromPackRow($barcode, '낱장', $exactSheet, 0, 0, 1);
        }

        $sheetRows = $this->db->table('bag_receiving_pack_code')
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_sheet_start_code !=', '')
            ->where('brpc_sheet_end_code !=', '')
            ->limit(200)
            ->get()
            ->getResultArray();
        foreach ($sheetRows as $row) {
            $start = (string) ($row['brpc_sheet_start_code'] ?? '');
            $end = (string) ($row['brpc_sheet_end_code'] ?? '');
            if ($this->barcodeInRange($barcode, $start, $end)) {
                return $this->resolvedFromPackRow($barcode, '낱장', $row, 0, 0, 1);
            }
        }

        $fromScan = $this->resolveBarcodeFromScanTables($lgIdx, $barcode);
        if ($fromScan !== null) {
            return $fromScan;
        }

        return ['ok' => false, 'message' => '등록되지 않은 바코드입니다.'];
    }

    /**
     * 판매·반품 스캔에만 있는 낱장 코드(입고 팩 테이블 미등록) 조회
     *
     * @return array{ok: true, barcode: string, unit: string, bag_code: string, bag_name: string, lot_no: string, box_code: string, pack_code: string, pack_ids: list<int>, qty_box: int, qty_pack: int, qty_sheet: int}|null
     */
    private function resolveBarcodeFromScanTables(int $lgIdx, string $barcode): ?array
    {
        $bagCode = '';
        $bagName = '';
        if ($this->db->tableExists('bag_sale_scan_code')) {
            $sale = $this->db->table('bag_sale_scan_code')
                ->select('bssc_bag_code, bssc_bag_name')
                ->where('bssc_lg_idx', $lgIdx)
                ->where('bssc_code', $barcode)
                ->orderBy('bssc_regdate', 'DESC')
                ->limit(1)
                ->get()
                ->getRowArray();
            if (is_array($sale) && $sale !== []) {
                $bagCode = (string) ($sale['bssc_bag_code'] ?? '');
                $bagName = (string) ($sale['bssc_bag_name'] ?? '');
            }
        }
        if ($bagCode === '' && $this->db->tableExists('bag_return_scan_code')) {
            $ret = $this->db->table('bag_return_scan_code')
                ->select('brsc_bag_code, brsc_bag_name')
                ->where('brsc_lg_idx', $lgIdx)
                ->where('brsc_code', $barcode)
                ->orderBy('brsc_regdate', 'DESC')
                ->limit(1)
                ->get()
                ->getRowArray();
            if (is_array($ret) && $ret !== []) {
                $bagCode = (string) ($ret['brsc_bag_code'] ?? '');
                $bagName = (string) ($ret['brsc_bag_name'] ?? '');
            }
        }
        if ($bagCode === '' && $bagName === '') {
            return null;
        }

        $packRow = $this->findPackRowContainingSheet($lgIdx, $barcode);

        return [
            'ok'        => true,
            'barcode'   => $barcode,
            'unit'      => '낱장',
            'bag_code'  => $bagCode,
            'bag_name'  => $bagName,
            'lot_no'    => (string) ($packRow['brpc_lot_no'] ?? ''),
            'box_code'  => (string) ($packRow['brpc_box_code'] ?? ''),
            'pack_code' => (string) ($packRow['brpc_pack_code'] ?? ''),
            'pack_ids'  => isset($packRow['brpc_idx']) ? [(int) $packRow['brpc_idx']] : [],
            'qty_box'   => 0,
            'qty_pack'  => 0,
            'qty_sheet' => 1,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function findPackRowContainingSheet(int $lgIdx, string $barcode): array
    {
        if (! $this->db->tableExists('bag_receiving_pack_code')) {
            return [];
        }

        $exact = $this->db->table('bag_receiving_pack_code')
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_sheet_start_code', $barcode)
            ->limit(1)
            ->get()
            ->getRowArray();
        if (is_array($exact) && $exact !== []) {
            return $exact;
        }

        foreach ($this->db->table('bag_receiving_pack_code')
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_sheet_start_code !=', '')
            ->where('brpc_sheet_end_code !=', '')
            ->limit(200)
            ->get()
            ->getResultArray() as $row) {
            $start = (string) ($row['brpc_sheet_start_code'] ?? '');
            $end = (string) ($row['brpc_sheet_end_code'] ?? '');
            if ($this->barcodeInRange($barcode, $start, $end)) {
                return $row;
            }
        }

        return [];
    }

    /**
     * @param array<string, mixed> $pack
     * @return array{ok: true, barcode: string, unit: string, bag_code: string, bag_name: string, lot_no: string, box_code: string, pack_code: string, pack_ids: list<int>, qty_box: int, qty_pack: int, qty_sheet: int}
     */
    private function resolvedFromPackRow(string $barcode, string $unit, array $pack, int $qtyBox, int $qtyPack, int $qtySheet): array
    {
        return [
            'ok'        => true,
            'barcode'   => $barcode,
            'unit'      => $unit,
            'bag_code'  => (string) ($pack['brpc_bag_code'] ?? ''),
            'bag_name'  => (string) ($pack['brpc_bag_name'] ?? ''),
            'lot_no'    => (string) ($pack['brpc_lot_no'] ?? ''),
            'box_code'  => (string) ($pack['brpc_box_code'] ?? ''),
            'pack_code' => (string) ($pack['brpc_pack_code'] ?? ''),
            'pack_ids'  => [(int) ($pack['brpc_idx'] ?? 0)],
            'qty_box'   => $qtyBox,
            'qty_pack'  => $qtyPack,
            'qty_sheet' => $qtySheet,
        ];
    }

    /**
     * @param array<string, mixed> $resolved
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function collectFlowRows(int $lgIdx, array $resolved): array
    {
        $unit = (string) ($resolved['unit'] ?? '');

        return match ($unit) {
            '낱장' => $this->collectFlowRowsForSheet($lgIdx, $resolved),
            '박스' => $this->collectFlowRowsForBox($lgIdx, $resolved),
            default => $this->collectFlowRowsForPack($lgIdx, $resolved),
        };
    }

    /**
     * 낱장: 해당 바코드 판매·반품만 + 소속 팩 입고 1건(발주·동일 LOT 전체 이력 제외)
     *
     * @param array<string, mixed> $resolved
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function collectFlowRowsForSheet(int $lgIdx, array $resolved): array
    {
        $rows = [];
        $barcode = trim((string) ($resolved['barcode'] ?? ''));
        if ($barcode === '') {
            return [];
        }

        $brIdx = $this->receivingBrIdxForPackCode($lgIdx, (string) ($resolved['pack_code'] ?? ''));
        if ($brIdx > 0) {
            foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
                $rows[] = $ev;
            }
        }

        foreach ($this->loadScanEventsForCodes($lgIdx, [$barcode]) as $ev) {
            $rows[] = $ev;
        }
        foreach ($this->loadReturnEventsForCodes($lgIdx, [$barcode]) as $ev) {
            $rows[] = $ev;
        }

        return $rows;
    }

    /**
     * 팩: 팩·낱장 바코드 스캔 + 입고 + LOT 발주
     *
     * @param array<string, mixed> $resolved
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function collectFlowRowsForPack(int $lgIdx, array $resolved): array
    {
        $rows = [];
        $codes = array_values(array_unique(array_filter([
            (string) ($resolved['barcode'] ?? ''),
            (string) ($resolved['pack_code'] ?? ''),
        ], static fn (string $c): bool => $c !== '')));

        $brIdx = $this->receivingBrIdxForPackCode($lgIdx, (string) ($resolved['pack_code'] ?? ''));
        if ($brIdx > 0) {
            foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
                $rows[] = $ev;
            }
        }

        foreach ($this->loadScanEventsForCodes($lgIdx, $codes) as $ev) {
            $rows[] = $ev;
        }
        foreach ($this->loadReturnEventsForCodes($lgIdx, $codes) as $ev) {
            $rows[] = $ev;
        }

        $lotNo = (string) ($resolved['lot_no'] ?? '');
        if ($lotNo !== '') {
            foreach ($this->loadOrderEventsForLot($lgIdx, $lotNo) as $ev) {
                $rows[] = $ev;
            }
        }

        return $rows;
    }

    /**
     * 박스: 박스·소속 팩 코드 스캔 + 입고(박스 내 팩) + LOT 발주
     *
     * @param array<string, mixed> $resolved
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function collectFlowRowsForBox(int $lgIdx, array $resolved): array
    {
        $rows = [];
        $boxCode = (string) ($resolved['box_code'] ?? '');
        $codes = array_values(array_unique(array_filter([
            (string) ($resolved['barcode'] ?? ''),
            $boxCode,
            (string) ($resolved['pack_code'] ?? ''),
        ], static fn (string $c): bool => $c !== '')));

        if ($boxCode !== '' && $this->db->tableExists('bag_receiving_pack_code')) {
            $packCodes = $this->db->table('bag_receiving_pack_code')
                ->select('brpc_pack_code')
                ->where('brpc_lg_idx', $lgIdx)
                ->where('brpc_box_code', $boxCode)
                ->get()
                ->getResultArray();
            foreach ($packCodes as $p) {
                $pc = (string) ($p['brpc_pack_code'] ?? '');
                if ($pc !== '' && ! in_array($pc, $codes, true)) {
                    $codes[] = $pc;
                }
            }
        }

        $seenBr = [];
        if ($boxCode !== '' && $this->db->tableExists('bag_receiving_pack_code')) {
            $brRows = $this->db->table('bag_receiving_pack_code')
                ->select('brpc_br_idx')
                ->distinct()
                ->where('brpc_lg_idx', $lgIdx)
                ->where('brpc_box_code', $boxCode)
                ->get()
                ->getResultArray();
            foreach ($brRows as $brRow) {
                $brIdx = (int) ($brRow['brpc_br_idx'] ?? 0);
                if ($brIdx <= 0 || isset($seenBr[$brIdx])) {
                    continue;
                }
                $seenBr[$brIdx] = true;
                foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
                    $rows[] = $ev;
                }
            }
        }

        foreach ($this->loadScanEventsForCodes($lgIdx, $codes) as $ev) {
            $rows[] = $ev;
        }
        foreach ($this->loadReturnEventsForCodes($lgIdx, $codes) as $ev) {
            $rows[] = $ev;
        }

        $lotNo = (string) ($resolved['lot_no'] ?? '');
        if ($lotNo !== '') {
            foreach ($this->loadOrderEventsForLot($lgIdx, $lotNo) as $ev) {
                $rows[] = $ev;
            }
        }

        return $rows;
    }

    private function receivingBrIdxForPackCode(int $lgIdx, string $packCode): int
    {
        if ($packCode === '' || ! $this->db->tableExists('bag_receiving_pack_code')) {
            return 0;
        }

        $p = $this->db->table('bag_receiving_pack_code')
            ->select('brpc_br_idx')
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_pack_code', $packCode)
            ->get()
            ->getRowArray();

        return (int) ($p['brpc_br_idx'] ?? 0);
    }

    /**
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function loadReceivingEventsByBrIdx(int $lgIdx, int $brIdx): array
    {
        $sql = "
            SELECT r.br_receive_date, r.br_sender_name, r.br_regdate,
                   o.bo_order_date, c.cp_name, sa.sa_name
            FROM bag_receiving r
            LEFT JOIN bag_order o ON o.bo_idx = r.br_bo_idx
            LEFT JOIN company c ON c.cp_idx = o.bo_company_idx
            LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx
            WHERE r.br_lg_idx = ? AND r.br_idx = ?
            LIMIT 20
        ";
        $rows = [];
        foreach ($this->db->query($sql, [$lgIdx, $brIdx])->getResultArray() as $r) {
            $rows[] = $this->makeEvent(
                (string) ($r['br_receive_date'] ?? ''),
                (string) ($r['br_regdate'] ?? ''),
                $this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name'),
                '입고'
            );
        }

        return $rows;
    }

    /**
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function loadReceivingEventsForLot(int $lgIdx, string $lotNo): array
    {
        $sql = "
            SELECT r.br_receive_date, r.br_sender_name, r.br_regdate, r.br_bag_name,
                   c.cp_name, sa.sa_name
            FROM bag_receiving r
            INNER JOIN bag_order o ON o.bo_idx = r.br_bo_idx AND o.bo_lot_no = ?
            LEFT JOIN company c ON c.cp_idx = o.bo_company_idx
            LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx
            WHERE r.br_lg_idx = ?
            ORDER BY r.br_receive_date ASC, r.br_idx ASC
            LIMIT 200
        ";
        $rows = [];
        foreach ($this->db->query($sql, [$lotNo, $lgIdx])->getResultArray() as $r) {
            $label = trim((string) ($r['br_bag_name'] ?? ''));
            $rows[] = $this->makeEvent(
                (string) ($r['br_receive_date'] ?? ''),
                (string) ($r['br_regdate'] ?? ''),
                $this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name') . ($label !== '' ? ' · ' . $label : ''),
                '입고'
            );
        }

        return $rows;
    }

    /**
     * @param list<string> $codes
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function loadScanEventsForCodes(int $lgIdx, array $codes): array
    {
        if ($codes === [] || ! $this->db->tableExists('bag_sale_scan_code')) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($codes), '?'));
        $params = array_merge([$lgIdx], $codes);
        $sql = "
            SELECT b.bssc_regdate, b.bssc_state, b.bssc_code, d.ds_name, d.ds_shop_no
            FROM bag_sale_scan_code b
            LEFT JOIN designated_shop d ON d.ds_idx = b.bssc_ds_idx
            WHERE b.bssc_lg_idx = ? AND b.bssc_code IN ({$placeholders})
            ORDER BY b.bssc_regdate ASC
            LIMIT 200
        ";
        $rows = [];
        foreach ($this->db->query($sql, $params)->getResultArray() as $r) {
            $state = strtolower((string) ($r['bssc_state'] ?? ''));
            $type = $state === 'in_stock' ? '반품입고' : '출고';
            $shop = trim((string) ($r['ds_name'] ?? ''));
            if ($shop === '') {
                $shop = trim((string) ($r['ds_shop_no'] ?? ''));
            }
            if ($shop === '') {
                $shop = '지정판매소';
            }
            $rows[] = $this->makeEvent(
                $this->dateOnly((string) ($r['bssc_regdate'] ?? '')),
                (string) ($r['bssc_regdate'] ?? ''),
                $shop,
                $type
            );
        }

        return $rows;
    }

    /**
     * @param list<string> $codes
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function loadReturnEventsForCodes(int $lgIdx, array $codes): array
    {
        if ($codes === [] || ! $this->db->tableExists('bag_return_scan_code')) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($codes), '?'));
        $params = array_merge([$lgIdx], $codes);
        $sql = "
            SELECT r.brsc_return_date, r.brsc_regdate, r.brsc_code, d.ds_name, d.ds_shop_no
            FROM bag_return_scan_code r
            LEFT JOIN designated_shop d ON d.ds_idx = r.brsc_ds_idx
            WHERE r.brsc_lg_idx = ? AND r.brsc_code IN ({$placeholders})
            ORDER BY r.brsc_return_date ASC
            LIMIT 200
        ";
        $rows = [];
        foreach ($this->db->query($sql, $params)->getResultArray() as $r) {
            $shop = trim((string) ($r['ds_name'] ?? ''));
            if ($shop === '') {
                $shop = trim((string) ($r['ds_shop_no'] ?? ''));
            }
            if ($shop === '') {
                $shop = '지정판매소';
            }
            $rows[] = $this->makeEvent(
                (string) ($r['brsc_return_date'] ?? ''),
                (string) ($r['brsc_regdate'] ?? ''),
                $shop,
                '반품'
            );
        }

        return $rows;
    }

    /**
     * @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
     */
    private function loadOrderEventsForLot(int $lgIdx, string $lotNo): array
    {
        $sql = "
            SELECT o.bo_order_date, o.bo_regdate, c.cp_name, sa.sa_name
            FROM bag_order o
            LEFT JOIN company c ON c.cp_idx = o.bo_company_idx
            LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx
            WHERE o.bo_lg_idx = ? AND o.bo_lot_no = ? AND o.bo_status = 'normal'
            ORDER BY o.bo_version DESC
            LIMIT 1
        ";
        $r = $this->db->query($sql, [$lgIdx, $lotNo])->getRowArray();
        if (! $r) {
            return [];
        }

        return [
            $this->makeEvent(
                (string) ($r['bo_order_date'] ?? ''),
                (string) ($r['bo_regdate'] ?? ''),
                $this->pickSource($r, '제작·발주', 'cp_name', 'sa_name'),
                '발주'
            ),
        ];
    }

    /**
     * @param array<string, mixed> $order
     * @return array<string, mixed>
     */
    private function buildLotFromOrderOnly(int $lgIdx, string $lotNo, array $order): array
    {
        $rows = $this->loadOrderEventsForLot($lgIdx, $lotNo);
        $boIdx = (int) ($order['bo_idx'] ?? 0);
        if ($boIdx > 0) {
            $sql = "
                SELECT r.br_receive_date, r.br_sender_name, r.br_regdate, r.br_bag_name,
                       c.cp_name, sa.sa_name
                FROM bag_receiving r
                LEFT JOIN bag_order o ON o.bo_idx = r.br_bo_idx
                LEFT JOIN company c ON c.cp_idx = o.bo_company_idx
                LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx
                WHERE r.br_bo_idx = ?
                ORDER BY r.br_receive_date ASC
            ";
            foreach ($this->db->query($sql, [$boIdx])->getResultArray() as $r) {
                $rows[] = $this->makeEvent(
                    (string) ($r['br_receive_date'] ?? ''),
                    (string) ($r['br_regdate'] ?? ''),
                    $this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name'),
                    '입고'
                );
            }
        }

        usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0));

        return array_merge($this->emptyResult($lotNo), [
            'ok'       => true,
            'lot_no'   => $lotNo,
            'barcode'  => $lotNo,
            'rows'     => $rows,
        ]);
    }

    /**
     * @param array<string, mixed> $row
     */
    private function pickSource(array $row, string $default, string ...$keys): string
    {
        foreach ($keys as $key) {
            $v = trim((string) ($row[$key] ?? ''));
            if ($v !== '') {
                return $v;
            }
        }

        return $default;
    }

    /**
     * @return array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}
     */
    private function makeEvent(string $dateYmd, string $sortDatetime, string $counterparty, string $flowType): array
    {
        $ts = strtotime($sortDatetime !== '' ? $sortDatetime : $dateYmd);

        return [
            'flow_date'    => $dateYmd !== '' ? $dateYmd : ($ts ? date('Y-m-d', $ts) : ''),
            'counterparty' => $counterparty,
            'flow_type'    => $flowType,
            'sort_ts'      => $ts !== false ? $ts : 0,
        ];
    }

    private function dateOnly(string $datetime): string
    {
        if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $datetime, $m) === 1) {
            return $m[1];
        }

        $ts = strtotime($datetime);

        return $ts ? date('Y-m-d', $ts) : $datetime;
    }

    /**
     * LOT 수불 조회 화면 테스트용 — 등록된 바코드·LOT 샘플
     *
     * @return list<array{kind: string, code: string, bag_name: string, lot_no: string, state: string, hint: string}>
     */
    public function loadTestSamples(int $lgIdx, int $limit = 80): array
    {
        $samples = [];
        $seen = [];

        $push = static function (array &$samples, array &$seen, string $kind, string $code, string $bagName, string $lotNo, string $state, string $hint) use ($limit): void {
            $code = trim($code);
            if ($code === '' || isset($seen[$code]) || count($samples) >= $limit) {
                return;
            }
            $seen[$code] = true;
            $samples[] = [
                'kind'     => $kind,
                'code'     => $code,
                'bag_name' => $bagName,
                'lot_no'   => $lotNo,
                'state'    => $state,
                'hint'     => $hint,
            ];
        };

        if ($this->db->tableExists('bag_receiving_pack_code')) {
            foreach ($this->db->table('bag_receiving_pack_code')
                ->select('brpc_pack_code, brpc_box_code, brpc_sheet_start_code, brpc_bag_name, brpc_lot_no, brpc_state')
                ->where('brpc_lg_idx', $lgIdx)
                ->orderBy('brpc_idx', 'DESC')
                ->limit(40)
                ->get()
                ->getResultArray() as $row) {
                $state = $this->packStateLabel((string) ($row['brpc_state'] ?? ''));
                $bagName = (string) ($row['brpc_bag_name'] ?? '');
                $lotNo = (string) ($row['brpc_lot_no'] ?? '');
                $push($samples, $seen, '팩', (string) ($row['brpc_pack_code'] ?? ''), $bagName, $lotNo, $state, '입고 팩 코드');
            }

            $boxRows = $this->db->query("
                SELECT brpc_box_code,
                       MAX(brpc_bag_name) AS brpc_bag_name,
                       MAX(brpc_lot_no) AS brpc_lot_no,
                       MAX(brpc_state) AS brpc_state
                FROM bag_receiving_pack_code
                WHERE brpc_lg_idx = ? AND brpc_box_code != ''
                GROUP BY brpc_box_code
                ORDER BY MAX(brpc_idx) DESC
                LIMIT 15
            ", [$lgIdx])->getResultArray();
            foreach ($boxRows as $row) {
                $push($samples, $seen, '박스', (string) ($row['brpc_box_code'] ?? ''), (string) ($row['brpc_bag_name'] ?? ''), (string) ($row['brpc_lot_no'] ?? ''), $this->packStateLabel((string) ($row['brpc_state'] ?? '')), '박스 단위 조회');
            }

            $sheetRows = $this->db->table('bag_receiving_pack_code')
                ->select('brpc_sheet_start_code, brpc_bag_name, brpc_lot_no, brpc_state')
                ->where('brpc_lg_idx', $lgIdx)
                ->where('brpc_sheet_start_code !=', '')
                ->orderBy('brpc_idx', 'DESC')
                ->limit(15)
                ->get()
                ->getResultArray();
            foreach ($sheetRows as $row) {
                $push($samples, $seen, '낱장', (string) ($row['brpc_sheet_start_code'] ?? ''), (string) ($row['brpc_bag_name'] ?? ''), (string) ($row['brpc_lot_no'] ?? ''), $this->packStateLabel((string) ($row['brpc_state'] ?? '')), '낱장 시작 코드');
            }

            $lotRows = $this->db->query("
                SELECT DISTINCT brpc_lot_no
                FROM bag_receiving_pack_code
                WHERE brpc_lg_idx = ? AND brpc_lot_no != ''
                ORDER BY brpc_lot_no DESC
                LIMIT 10
            ", [$lgIdx])->getResultArray();
            foreach ($lotRows as $row) {
                $lot = (string) ($row['brpc_lot_no'] ?? '');
                $push($samples, $seen, 'LOT', $lot, '(LOT 전체)', $lot, '—', 'lot_no 파라미터·입력 동일');
            }
        }

        if ($this->db->tableExists('bag_sale_scan_code')) {
            foreach ($this->db->table('bag_sale_scan_code b')
                ->select('b.bssc_code, b.bssc_bag_name, b.bssc_unit, b.bssc_state, b.bssc_regdate')
                ->where('b.bssc_lg_idx', $lgIdx)
                ->orderBy('b.bssc_regdate', 'DESC')
                ->limit(20)
                ->get()
                ->getResultArray() as $row) {
                $state = strtolower((string) ($row['bssc_state'] ?? '')) === 'sold' ? '판매' : '반품재고';
                $push($samples, $seen, '스캔', (string) ($row['bssc_code'] ?? ''), (string) ($row['bssc_bag_name'] ?? ''), '', $state, '판매·반품 스캔 이력');
            }
        }

        return $samples;
    }

    private function packStateLabel(string $state): string
    {
        return match (strtolower($state)) {
            'in_stock' => '재고',
            'sold'     => '판매',
            default    => $state !== '' ? $state : '—',
        };
    }

    private function barcodeInRange(string $code, string $start, string $end): bool
    {
        if ($start === '' || $end === '') {
            return false;
        }

        $extract = static function (string $v): array {
            if (preg_match('/^(.*?)(\d+)$/', $v, $m) === 1) {
                return [(string) $m[1], (int) $m[2], strlen((string) $m[2])];
            }

            return ['', -1, 0];
        };
        [$cp, $cn, $cl] = $extract($code);
        [$sp, $sn, $sl] = $extract($start);
        [$ep, $en, $el] = $extract($end);
        if ($cn >= 0 && $sn >= 0 && $en >= 0 && $cp === $sp && $sp === $ep && $cl === $sl && $sl === $el) {
            return $cn >= $sn && $cn <= $en;
        }

        return strcmp($code, $start) >= 0 && strcmp($code, $end) <= 0;
    }
}
