<?php

namespace App\Controllers\Admin;

use App\Controllers\BaseController;
use App\Models\BagOrderModel;
use App\Models\BagOrderItemModel;
use App\Models\BagPriceModel;
use App\Models\PackagingUnitModel;
use App\Models\CompanyModel;
use App\Models\SalesAgencyModel;
use App\Models\BagReceivingModel;
use App\Models\CodeKindModel;
use App\Models\CodeDetailModel;
use App\Models\LocalGovernmentModel;
use App\Libraries\Blockchain\SqlLedger;
class BagOrder extends BaseController
{
    private BagOrderModel $orderModel;
    private BagOrderItemModel $itemModel;

    public function __construct()
    {
        $this->orderModel = model(BagOrderModel::class);
        $this->itemModel  = model(BagOrderItemModel::class);
    }

    public function index()
    {
        helper('admin');
        $lgIdx = admin_effective_lg_idx();
        if (! $lgIdx) {
            return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
        }

        $startMonth = (string) ($this->request->getGet('start_month') ?? date('Y-m'));
        $endMonth   = (string) ($this->request->getGet('end_month') ?? date('Y-m'));
        if (! preg_match('/^\d{4}-\d{2}$/', $startMonth)) {
            $startMonth = date('Y-m');
        }
        if (! preg_match('/^\d{4}-\d{2}$/', $endMonth)) {
            $endMonth = $startMonth;
        }
        if (strtotime($startMonth . '-01') > strtotime($endMonth . '-01')) {
            [$startMonth, $endMonth] = [$endMonth, $startMonth];
        }

        $companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
        $bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
        $receiveType = (string) ($this->request->getGet('receive_type') ?? 'all');
        if (! in_array($receiveType, ['all', 'received', 'pending'], true)) {
            $receiveType = 'all';
        }

        $companies = model(CompanyModel::class)
            ->where('cp_lg_idx', $lgIdx)
            ->where('cp_type', '제작업체')
            ->where('cp_state', 1)
            ->orderBy('cp_name', 'ASC')
            ->findAll();
        $companyMap = [];
        foreach ($companies as $company) {
            $companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
        }

        $agencyMap = [];
        foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $agency) {
            $agencyMap[(int) $agency->sa_idx] = (string) ($agency->sa_name ?? '');
        }

        $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
        $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
        $bagNameMap = [];
        foreach ($bagCodes as $code) {
            $bagNameMap[(string) $code->cd_code] = (string) $code->cd_name;
        }

        $reportData = $this->buildOrderStatusRows(
            $lgIdx,
            $startMonth,
            $endMonth,
            $companyIdx,
            $bagCode,
            $receiveType,
            $companyMap,
            $agencyMap,
            $bagNameMap
        );

        return $this->renderWorkPage(
            '발주 현황',
            'admin/bag_order/index',
            [
                'startMonth' => $startMonth,
                'endMonth' => $endMonth,
                'companyIdx' => $companyIdx,
                'bagCode' => $bagCode,
                'receiveType' => $receiveType,
                'companyOptions' => $companies,
                'bagCodeOptions' => $bagCodes,
                'rows' => $reportData['rows'],
                'groupRows' => $reportData['groupRows'],
                'grandTotals' => $reportData['grandTotals'],
            ]
        );
    }

    public function export()
    {
        helper(['admin', 'export']);
        $lgIdx = admin_effective_lg_idx();
        if (! $lgIdx) {
            return redirect()->to(mgmt_url('bag-orders'))->with('error', '지자체를 선택해 주세요.');
        }

        $startMonth = (string) ($this->request->getGet('start_month') ?? date('Y-m'));
        $endMonth   = (string) ($this->request->getGet('end_month') ?? date('Y-m'));
        if (! preg_match('/^\d{4}-\d{2}$/', $startMonth)) {
            $startMonth = date('Y-m');
        }
        if (! preg_match('/^\d{4}-\d{2}$/', $endMonth)) {
            $endMonth = $startMonth;
        }
        if (strtotime($startMonth . '-01') > strtotime($endMonth . '-01')) {
            [$startMonth, $endMonth] = [$endMonth, $startMonth];
        }

        $companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
        $bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
        $receiveType = (string) ($this->request->getGet('receive_type') ?? 'all');
        if (! in_array($receiveType, ['all', 'received', 'pending'], true)) {
            $receiveType = 'all';
        }

        $companyMap = [];
        foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->findAll() as $company) {
            $companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
        }
        $agencyMap = [];
        foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $agency) {
            $agencyMap[(int) $agency->sa_idx] = (string) ($agency->sa_name ?? '');
        }
        $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
        $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
        $bagNameMap = [];
        foreach ($bagCodes as $code) {
            $bagNameMap[(string) $code->cd_code] = (string) $code->cd_name;
        }

        $reportData = $this->buildOrderStatusRows(
            $lgIdx,
            $startMonth,
            $endMonth,
            $companyIdx,
            $bagCode,
            $receiveType,
            $companyMap,
            $agencyMap,
            $bagNameMap
        );

        $rows = [];
        foreach ($reportData['rows'] as $row) {
            if (! empty($row['is_subtotal'])) {
                $rows[] = [
                    '',
                    '',
                    (string) ($row['label'] ?? '소계'),
                    (int) ($row['order_qty'] ?? 0),
                    (int) ($row['received_qty'] ?? 0),
                    (int) ($row['pending_qty'] ?? 0),
                    (float) ($row['amount'] ?? 0),
                    '',
                ];

                continue;
            }
            $rows[] = [
                (string) ($row['order_date'] ?? ''),
                (string) ($row['company_name'] ?? ''),
                (string) ($row['bag_name'] ?? ''),
                (int) ($row['order_qty'] ?? 0),
                (int) ($row['received_qty'] ?? 0),
                (int) ($row['pending_qty'] ?? 0),
                (float) ($row['amount'] ?? 0),
                (string) ($row['agency_name'] ?? ''),
                '',
            ];
        }

        $gt = $reportData['grandTotals'] ?? [];
        $rows[] = [
            '',
            '',
            '총계',
            (int) ($gt['order_qty'] ?? 0),
            (int) ($gt['received_qty'] ?? 0),
            (int) ($gt['pending_qty'] ?? 0),
            (float) ($gt['amount'] ?? 0),
            '',
            '',
        ];

        export_xlsx(
            '발주현황_' . date('Ymd'),
            '발주현황',
            ['발주일자', '제작업체', '품명', '발주수량', '입고수량', '미입고수량', '발주금액', '입고처', '비고'],
            $rows
        );
    }

    /**
     * 발주 현황(품목 기준) 행 및 소계를 만든다.
     */
    private function buildOrderStatusRows(
        int $lgIdx,
        string $startMonth,
        string $endMonth,
        int $companyIdx,
        string $bagCode,
        string $receiveType,
        array $companyMap,
        array $agencyMap,
        array $bagNameMap
    ): array {
        $startDate = $startMonth . '-01';
        $endDate = date('Y-m-t', strtotime($endMonth . '-01 00:00:00'));

        $builder = $this->orderModel
            ->where('bo_lg_idx', $lgIdx)
            ->whereLatestHead($lgIdx)
            ->where('bo_order_date >=', $startDate)
            ->where('bo_order_date <=', $endDate)
            ->whereIn('bo_status', ['normal', 'cancelled'])
            ->orderBy('bo_order_date', 'DESC')
            ->orderBy('bo_idx', 'DESC');
        if ($companyIdx > 0) {
            $builder->where('bo_company_idx', $companyIdx);
        }
        $orders = $builder->findAll();

        if (empty($orders)) {
            return ['rows' => [], 'groupRows' => [], 'grandTotals' => ['order_qty' => 0, 'received_qty' => 0, 'pending_qty' => 0, 'amount' => 0]];
        }

        $orderIds = array_map(static fn($order) => (int) $order->bo_idx, $orders);

        $itemsByOrder = [];
        if (! empty($orderIds)) {
            $allItems = $this->itemModel
                ->whereIn('boi_bo_idx', $orderIds)
                ->orderBy('boi_bo_idx', 'DESC')
                ->orderBy('boi_idx', 'ASC')
                ->findAll();
            foreach ($allItems as $item) {
                $boIdx = (int) ($item->boi_bo_idx ?? 0);
                if (! isset($itemsByOrder[$boIdx])) {
                    $itemsByOrder[$boIdx] = [];
                }
                $itemsByOrder[$boIdx][] = $item;
            }
        }

        $receivedMap = [];
        $receivingRows = model(BagReceivingModel::class)
            ->select('br_bo_idx, br_bag_code, SUM(br_qty_sheet) as recv_qty')
            ->where('br_lg_idx', $lgIdx)
            ->whereIn('br_bo_idx', $orderIds)
            ->groupBy('br_bo_idx, br_bag_code')
            ->findAll();
        foreach ($receivingRows as $received) {
            $key = (int) ($received->br_bo_idx ?? 0) . '|' . (string) ($received->br_bag_code ?? '');
            $receivedMap[$key] = (int) ($received->recv_qty ?? 0);
        }

        $rows = [];
        $groupRows = [];
        $grandTotals = ['order_qty' => 0, 'received_qty' => 0, 'pending_qty' => 0, 'amount' => 0.0];

        foreach ($orders as $order) {
            $boIdx = (int) ($order->bo_idx ?? 0);
            $items = $itemsByOrder[$boIdx] ?? [];
            $groupCount = 0;
            $groupTotalOrder = 0;
            $groupTotalReceived = 0;
            $groupTotalPending = 0;
            $groupTotalAmount = 0.0;

            foreach ($items as $item) {
                $itemBagCode = (string) ($item->boi_bag_code ?? '');
                if ($bagCode !== '' && $itemBagCode !== $bagCode) {
                    continue;
                }

                $orderQty = (int) ($item->boi_qty_sheet ?? 0);
                $recvQty = (int) ($receivedMap[$boIdx . '|' . $itemBagCode] ?? 0);
                if ($recvQty > $orderQty) {
                    $recvQty = $orderQty;
                }
                $pendingQty = max(0, $orderQty - $recvQty);

                if ($receiveType === 'received' && $recvQty <= 0) {
                    continue;
                }
                if ($receiveType === 'pending' && $pendingQty <= 0) {
                    continue;
                }

                $amount = (float) ($item->boi_amount ?? 0);
                $rows[] = [
                    'bo_idx' => $boIdx,
                    'order_date' => (string) ($order->bo_order_date ?? ''),
                    'company_name' => (string) ($companyMap[(int) ($order->bo_company_idx ?? 0)] ?? ''),
                    'bag_name' => (string) ($item->boi_bag_name ?? ($bagNameMap[$itemBagCode] ?? $itemBagCode)),
                    'order_qty' => $orderQty,
                    'received_qty' => $recvQty,
                    'pending_qty' => $pendingQty,
                    'amount' => $amount,
                    'agency_name' => (string) ($agencyMap[(int) ($order->bo_agency_idx ?? 0)] ?? ''),
                ];

                $groupCount++;
                $groupTotalOrder += $orderQty;
                $groupTotalReceived += $recvQty;
                $groupTotalPending += $pendingQty;
                $groupTotalAmount += $amount;
            }

            if ($groupCount > 0) {
                $groupRows[$boIdx] = $groupCount;
                $rows[] = [
                    'bo_idx' => $boIdx,
                    'is_subtotal' => true,
                    'label' => '소계',
                    'order_qty' => $groupTotalOrder,
                    'received_qty' => $groupTotalReceived,
                    'pending_qty' => $groupTotalPending,
                    'amount' => $groupTotalAmount,
                ];
                $grandTotals['order_qty'] += $groupTotalOrder;
                $grandTotals['received_qty'] += $groupTotalReceived;
                $grandTotals['pending_qty'] += $groupTotalPending;
                $grandTotals['amount'] += $groupTotalAmount;
            }
        }

        return ['rows' => $rows, 'groupRows' => $groupRows, 'grandTotals' => $grandTotals];
    }

    public function create()
    {
        helper('admin');
        $lgIdx = admin_effective_lg_idx();
        if (! $lgIdx) {
            return redirect()->to(mgmt_url('bag-orders'))->with('error', '지자체를 선택해 주세요.');
        }

        // 봉투 종류 + 단가 + 포장단위
        $kind     = model(CodeKindModel::class)->where('ck_code', 'O')->first();
        $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
        $priceMapRows = model(BagPriceModel::class)->latestActiveMapByBagCode($lgIdx);
        $units    = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll();

        $companies    = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll();
        $associations = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '협회')->where('cp_state', 1)->findAll();
        $agencies     = model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll();

        $companyMap = [];
        foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->findAll() as $company) {
            $companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
        }
        $agencyMap = [];
        foreach ($agencies as $agency) {
            $agencyMap[(int) $agency->sa_idx] = '[' . ($agency->sa_kind ?? '') . '] ' . ($agency->sa_code ?? '') . ' — ' . ($agency->sa_name ?? '');
        }

        $recentOrders = $this->orderModel
            ->where('bo_lg_idx', $lgIdx)
            ->whereLatestHead($lgIdx)
            ->orderBy('bo_order_date', 'DESC')
            ->orderBy('bo_idx', 'DESC')
            ->findAll(12);

        $bagNameMap = [];
        foreach ($bagCodes as $codeDetail) {
            $bagNameMap[(string) $codeDetail->cd_code] = (string) $codeDetail->cd_name;
        }
        $priceMap = [];
        foreach ($priceMapRows as $bagCode => $price) {
            $priceMap[(string) $bagCode] = (float) ($price->bp_order_price ?? 0);
        }
        $unitMap = [];
        foreach ($units as $unit) {
            $unitMap[(string) $unit->pu_bag_code] = [
                'boxPerPack' => (int) $unit->pu_box_per_pack,
                'packPerSheet' => (int) $unit->pu_pack_per_sheet,
                'totalPerBox' => (int) $unit->pu_total_per_box,
            ];
        }

        $bagReferenceRows = [];
        foreach ($bagCodes as $codeDetail) {
            $bagCode = (string) $codeDetail->cd_code;
            $unit    = $unitMap[$bagCode] ?? ['boxPerPack' => 0, 'packPerSheet' => 0, 'totalPerBox' => 0];
            $bagReferenceRows[] = [
                'code' => $bagCode,
                'name' => (string) ($bagNameMap[$bagCode] ?? ''),
                'orderPrice' => (float) ($priceMap[$bagCode] ?? 0),
                'boxPerPack' => (int) $unit['boxPerPack'],
                'packPerSheet' => (int) $unit['packPerSheet'],
                'totalPerBox' => (int) $unit['totalPerBox'],
            ];
        }

        return $this->renderWorkPage(
            '발주 등록',
            'admin/bag_order/create',
            compact(
                'bagCodes',
                'units',
                'companies',
                'associations',
                'agencies',
                'recentOrders',
                'companyMap',
                'agencyMap',
                'bagReferenceRows'
            )
        );
    }

    public function revise(int $id)
    {
        helper('admin');
        $lgIdx = admin_effective_lg_idx();
        $order = $this->orderModel->find($id);
        if (! $order || (int) $order->bo_lg_idx !== $lgIdx) {
            return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
        }

        return redirect()->to(site_url('bag/order/revise/' . $id));
    }

    public function store()
    {
        helper('admin');
        $lgIdx = admin_effective_lg_idx();
        if ($lgIdx === null || $lgIdx <= 0) {
            return redirect()->back()->withInput()->with('error', '지자체를 선택해 주세요.');
        }

        $sourceIdx = (int) ($this->request->getPost('bo_source_idx') ?? 0);
        $sourceOrder = null;
        if ($sourceIdx > 0) {
            $sourceOrder = $this->orderModel->find($sourceIdx);
            if (! $sourceOrder || (int) $sourceOrder->bo_lg_idx !== $lgIdx) {
                return redirect()->back()->withInput()->with('error', '수정 대상 발주를 찾을 수 없습니다.');
            }
        }

        $rules = [
            'bo_order_date'  => 'required|valid_date[Y-m-d]',
            'bo_company_idx' => 'permit_empty|is_natural_no_zero',
            'bo_agency_idx'  => 'permit_empty|is_natural_no_zero',
        ];
        if (! $this->validate($rules)) {
            return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
        }

        $bagCodes  = $this->request->getPost('item_bag_code') ?? [];
        $qtySheets = $this->request->getPost('item_qty_sheet') ?? [];
        $qtyBoxes  = $this->request->getPost('item_qty_box') ?? []; // 구 화면 호환
        $postedUnitPrices = $this->request->getPost('item_unit_price');
        $changeKind       = (string) ($this->request->getPost('bo_change_mode') ?? 'meta');
        if (! in_array($changeKind, ['price', 'meta', 'delete'], true)) {
            $changeKind = 'meta';
        }
        $itemCount = count($bagCodes);
        $normalizedItems = [];
        for ($i = 0; $i < $itemCount; $i++) {
            $code = trim((string) ($bagCodes[$i] ?? ''));
            $qtySheet = (int) ($qtySheets[$i] ?? 0);
            $qtyBox = (int) ($qtyBoxes[$i] ?? 0);
            if ($code === '' || ($qtySheet <= 0 && $qtyBox <= 0)) {
                continue;
            }
            $normalizedItems[] = ['code' => $code, 'qtySheet' => $qtySheet, 'qtyBox' => $qtyBox];
        }
        if (empty($normalizedItems)) {
            return redirect()->back()->withInput()->with('error', '최소 1개 이상의 봉투 수량을 입력해 주세요.');
        }

        $priceByCode = [];
        if ($sourceOrder !== null && $changeKind === 'price' && is_array($postedUnitPrices)) {
            for ($pi = 0; $pi < count($bagCodes); $pi++) {
                $c = trim((string) ($bagCodes[$pi] ?? ''));
                if ($c === '') {
                    continue;
                }
                $raw = $postedUnitPrices[$pi] ?? null;
                if ($raw !== null && $raw !== '' && is_numeric($raw)) {
                    $priceByCode[$c] = round((float) $raw, 2);
                }
            }
        }

        $db = \Config\Database::connect();
        $db->transStart();

        try {
        if ($sourceOrder) {
            $uuid = (string) $sourceOrder->bo_uuid;
            $maxVerRow = $this->orderModel->selectMax('bo_version')->where('bo_uuid', $uuid)->first();
            $latestVersion = ($maxVerRow !== null && isset($maxVerRow->bo_version)) ? (int) $maxVerRow->bo_version : 0;
            $version = $latestVersion + 1;
            $lotNo = (string) $sourceOrder->bo_lot_no;
        } else {
            $uuid = $this->generateUuidV4();
            $version = 1;
            $lotNo = $this->generateLotNo6();
        }

        $orderData = [
            'bo_uuid'        => $uuid,
            'bo_version'     => $version,
            'bo_lg_idx'      => $lgIdx,
            'bo_gugun_code'  => $this->resolveGugunCodeFromLg($lgIdx),
            'bo_dong_code'   => '',
            'bo_company_idx' => $this->request->getPost('bo_company_idx') ?: null,
            'bo_agency_idx'  => $this->request->getPost('bo_agency_idx') ?: null,
            'bo_fee_rate'    => (float) ($this->request->getPost('bo_fee_rate') ?: 0),
            'bo_order_date'  => $this->request->getPost('bo_order_date'),
            'bo_bag_types'   => '',
            'bo_unit_prices' => '',
            'bo_qty_boxes'   => '',
            'bo_lot_no'      => $lotNo,
            'bo_status'      => 'normal',
            'bo_orderer_idx' => session()->get('mb_idx'),
            'bo_regdate'     => date('Y-m-d H:i:s'),
        ];

        // 발주담당(manager.mg_idx) — 컬럼이 있을 때만 저장
        if (\Config\Database::connect()->fieldExists('bo_manager_idx', 'bag_order')) {
            $orderData['bo_manager_idx'] = $this->request->getPost('bo_manager_idx') ?: null;
        }

        // 품목 입력 후 최종 payload 기준으로 해시를 계산하므로 우선 빈값으로 생성
        $orderData['bo_hash'] = '';

        $this->orderModel->insert($orderData);
        $boIdx = (int) $this->orderModel->getInsertID();

        // 품목 저장
        $hashItems = [];
        $bagTypesForHeader = [];
        $unitPricesForHeader = [];
        $qtyBoxesForHeader = [];
        foreach ($normalizedItems as $item) {
            $code = $item['code'];
            $qtySheetInput = (int) ($item['qtySheet'] ?? 0);
            $qtyBoxInput = (int) ($item['qtyBox'] ?? 0);
            // 포장단위에서 낱장 환산
            $unit = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $code)->where('pu_state', 1)->first();
            $totalPerBox = $unit ? max(1, (int) $unit->pu_total_per_box) : 1;
            $qtySheet = $qtySheetInput > 0 ? $qtySheetInput : ($qtyBoxInput * $totalPerBox);
            if ($qtySheet <= 0) {
                continue;
            }
            $qtyBox = intdiv($qtySheet, $totalPerBox);

            // 단가 (발주 변경·단가 구분 시 POST 단가 우선)
            $price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, $code);
            $unitPrice = $price ? (float) $price->bp_order_price : 0;
            if ($sourceOrder !== null && isset($priceByCode[$code])) {
                $unitPrice = $priceByCode[$code];
            }

            // 봉투명
            $kindO  = model(CodeKindModel::class)->where('ck_code', 'O')->first();
            $detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $code, $lgIdx) : null;

            $itemData = [
                'boi_bo_idx'     => $boIdx,
                'boi_bag_code'   => $code,
                'boi_bag_name'   => $detail ? $detail->cd_name : '',
                'boi_unit_price' => $unitPrice,
                'boi_qty_box'    => $qtyBox,
                'boi_qty_sheet'  => $qtySheet,
                'boi_amount'     => $unitPrice * $qtySheet,
            ];
            $this->itemModel->insert($itemData);
            $hashItems[] = $itemData;

            $bagTypesForHeader[] = [
                'code' => $itemData['boi_bag_code'],
                'name' => $itemData['boi_bag_name'],
            ];
            $unitPricesForHeader[] = [
                'code' => $itemData['boi_bag_code'],
                'unit_price' => $itemData['boi_unit_price'],
            ];
            $qtyBoxesForHeader[] = [
                'code' => $itemData['boi_bag_code'],
                'qty_box' => $itemData['boi_qty_box'],
            ];
        }

        $orderData['bo_bag_types'] = json_encode($bagTypesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
        $orderData['bo_unit_prices'] = json_encode($unitPricesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
        $orderData['bo_qty_boxes'] = json_encode($qtyBoxesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';

        // 최종 발주 데이터(헤더+품목) 해시
        $hashPayload = $orderData;
        $hashPayload['bo_idx'] = $boIdx;
        $hashPayload['items'] = $hashItems;
        $hashJson = json_encode($hashPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $orderHash = hash('sha256', $hashJson !== false ? $hashJson : (string) $boIdx);
        $this->orderModel->update($boIdx, [
            'bo_bag_types' => $orderData['bo_bag_types'],
            'bo_unit_prices' => $orderData['bo_unit_prices'],
            'bo_qty_boxes' => $orderData['bo_qty_boxes'],
            'bo_hash' => $orderHash,
        ]);

        $beforeHash = $sourceOrder ? (string) ($sourceOrder->bo_hash ?? '') : '';
        $seedFilePath = $this->generateBarcodeSeedFile($uuid, $version, $lotNo, $orderData, $hashItems, $orderHash);
        $blockPayload = [
            'bo_idx' => $boIdx,
            'bo_uuid' => $uuid,
            'bo_version' => $version,
            'bo_lot_no' => $lotNo,
            'bo_hash' => $orderHash,
            'seed_file' => $seedFilePath,
            'hash_chain' => $beforeHash !== '' ? [$beforeHash, $orderHash] : [$orderHash],
            'order' => $orderData,
            'items' => $hashItems,
        ];
        $ledger = new SqlLedger();
        $ledger->appendBlock(
            $sourceOrder ? 'ORDER_UPDATE' : 'ORDER_CREATE',
            $blockPayload,
            $uuid,
            $version,
            session()->get('mb_idx') ? (int) session()->get('mb_idx') : null,
            $lgIdx
        );

        // CT-05: 감사 로그
        helper('audit');
        if ($sourceOrder) {
            audit_log(
                'update',
                'bag_order',
                $boIdx,
                ['bo_idx' => (int) $sourceOrder->bo_idx, 'bo_hash' => $beforeHash, 'bo_version' => (int) $sourceOrder->bo_version],
                array_merge($orderData, ['bo_idx' => $boIdx, 'bo_hash' => $orderHash, 'items' => $hashItems, 'seed_file' => $seedFilePath])
            );
        } else {
            audit_log('create', 'bag_order', $boIdx, null, array_merge($orderData, ['bo_idx' => $boIdx, 'bo_hash' => $orderHash, 'items' => $hashItems, 'seed_file' => $seedFilePath]));
        }

        if (! $db->transComplete()) {
            throw new \RuntimeException('Transaction did not complete');
        }
        } catch (\Throwable $e) {
            $db->transRollback();
            log_message('error', 'BagOrder::store: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());

            return redirect()->back()->withInput()->with('error', '발주 저장 중 오류가 발생했습니다. 잠시 후 다시 시도하거나 관리자에게 문의하세요.');
        }

        return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 등록되었습니다. LOT: ' . $lotNo);
    }

    /** 효과 지자체(`local_government`)의 행정 구·군 코드(lg_code) */
    private function resolveGugunCodeFromLg(int $lgIdx): string
    {
        $lg = model(LocalGovernmentModel::class)->find($lgIdx);

        return $lg ? trim((string) ($lg->lg_code ?? '')) : '';
    }

    private function generateUuidV4(): string
    {
        $bytes = random_bytes(16);
        $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
        $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
    }

    private function generateLotNo6(): string
    {
        // 문서의 "LOT 번호 6 Byte" 요구를 맞추기 위해 영숫자 6자리로 생성한다.
        // 충돌 가능성을 낮추기 위해 최대 20회 재시도 후 timestamp 기반으로 fallback.
        $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        for ($attempt = 0; $attempt < 20; $attempt++) {
            $lot = '';
            for ($i = 0; $i < 6; $i++) {
                $lot .= $chars[random_int(0, strlen($chars) - 1)];
            }

            $exists = $this->orderModel->where('bo_lot_no', $lot)->countAllResults() > 0;
            if (! $exists) {
                return $lot;
            }
        }

        return strtoupper(substr(base_convert((string) time(), 10, 36), -6));
    }

    /**
     * @param array<string,mixed> $orderData
     * @param array<int,array<string,mixed>> $items
     */
    private function generateBarcodeSeedFile(string $uuid, int $version, string $lotNo, array $orderData, array $items, string $orderHash): string
    {
        $baseDir = WRITEPATH . 'barcode-seeds';
        if (! is_dir($baseDir)) {
            mkdir($baseDir, 0775, true);
        }
        $keyDir = WRITEPATH . 'keys';
        if (! is_dir($keyDir)) {
            mkdir($keyDir, 0775, true);
        }

        $privateKeyPath = $keyDir . DIRECTORY_SEPARATOR . 'barcode_seed_private.pem';
        $publicKeyPath = $keyDir . DIRECTORY_SEPARATOR . 'barcode_seed_public.pem';
        if (! is_file($privateKeyPath) || ! is_file($publicKeyPath)) {
            $config = [
                'private_key_bits' => 2048,
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
            ];
            $resource = openssl_pkey_new($config);
            if ($resource !== false) {
                $privatePem = '';
                openssl_pkey_export($resource, $privatePem);
                $details = openssl_pkey_get_details($resource);
                $publicPem = $details['key'] ?? '';
                if ($privatePem !== '' && $publicPem !== '') {
                    file_put_contents($privateKeyPath, $privatePem);
                    file_put_contents($publicKeyPath, $publicPem);
                }
            }
        }

        $payload = [
            'uuid' => $uuid,
            'version' => $version,
            'lot_no' => $lotNo,
            'order_hash' => $orderHash,
            'order' => $orderData,
            'items' => $items,
        ];
        $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}';

        $aesKey = random_bytes(32);
        $iv = random_bytes(16);
        $cipherRaw = openssl_encrypt($payloadJson, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
        if ($cipherRaw === false) {
            $cipherRaw = $payloadJson;
        }
        $encryptedKey = '';
        $publicPem = is_file($publicKeyPath) ? file_get_contents($publicKeyPath) : '';
        if (is_string($publicPem) && $publicPem !== '') {
            openssl_public_encrypt($aesKey, $encryptedKey, $publicPem, OPENSSL_PKCS1_OAEP_PADDING);
        }

        $seed = [
            'algorithm' => ['symmetric' => 'AES-256-CBC', 'asymmetric' => 'RSA-2048'],
            'lot_no' => $lotNo,
            'uuid' => $uuid,
            'version' => $version,
            'iv_b64' => base64_encode($iv),
            'key_b64' => $encryptedKey !== '' ? base64_encode($encryptedKey) : '',
            'cipher_b64' => base64_encode((string) $cipherRaw),
            'payload_hash' => hash('sha256', $payloadJson),
            'created_at' => date('c'),
        ];

        $fileName = $lotNo . '_v' . $version . '.seed.json';
        $fullPath = $baseDir . DIRECTORY_SEPARATOR . $fileName;
        file_put_contents($fullPath, json_encode($seed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

        return $fullPath;
    }

    public function detail(int $id)
    {
        helper('admin');
        $order = $this->orderModel->find($id);
        if (!$order || (int) $order->bo_lg_idx !== admin_effective_lg_idx()) {
            return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
        }

        $items = $this->itemModel->where('boi_bo_idx', $id)->findAll();

        $companyName = '';
        if ($order->bo_company_idx) {
            $c = model(CompanyModel::class)->find($order->bo_company_idx);
            $companyName = $c ? $c->cp_name : '';
        }
        $agencyName = '';
        if ($order->bo_agency_idx) {
            $a = model(SalesAgencyModel::class)->find($order->bo_agency_idx);
            if ($a) {
                $agencyName = '[' . ($a->sa_kind ?? '') . '] ' . ($a->sa_code ?? '') . ' — ' . ($a->sa_name ?? '');
            }
        }

        return $this->renderWorkPage('발주 상세 — ' . $order->bo_lot_no, 'admin/bag_order/detail', compact('order', 'items', 'companyName', 'agencyName'));
    }

    public function cancel(int $id)
    {
        helper('admin');
        $order = $this->orderModel->find($id);
        if (!$order || (int) $order->bo_lg_idx !== admin_effective_lg_idx()) {
            return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
        }

        $before = (array) $order;
        $beforeHash = (string) ($order->bo_hash ?? '');
        $this->appendLedgerForStatusChange($order, $id, 'ORDER_CANCEL', 'cancelled', $beforeHash);
        $after = (array) $this->orderModel->find($id);
        helper('audit');
        audit_log('update', 'bag_order', $id, $before, $after);

        return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 취소되었습니다.');
    }

    public function delete(int $id)
    {
        helper('admin');
        $order = $this->orderModel->find($id);
        if (!$order || (int) $order->bo_lg_idx !== admin_effective_lg_idx()) {
            return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
        }

        $before = (array) $order;
        $beforeHash = (string) ($order->bo_hash ?? '');
        $this->appendLedgerForStatusChange($order, $id, 'ORDER_DELETE', 'deleted', $beforeHash);
        $after = (array) $this->orderModel->find($id);
        helper('audit');
        audit_log('delete', 'bag_order', $id, $before, $after);

        return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 삭제 처리되었습니다.');
    }

    /**
     * 상태 변경 시(취소/삭제) 무결성 검증을 위해 bo_hash 재계산 후
     * SQL-Ledger(append-only)에 블록을 추가한다.
     *
     * @param object $order
     * @param int $boIdx
     * @param string $txType ORDER_CANCEL|ORDER_DELETE
     * @param string $newStatus cancelled|deleted
     * @param string $previousHash
     */
    private function appendLedgerForStatusChange(object $order, int $boIdx, string $txType, string $newStatus, string $previousHash): void
    {
        // 품목은 상태 변경 시 그대로이므로, 동일 payload 형태로 items array를 만든다.
        $items = $this->itemModel->where('boi_bo_idx', $boIdx)->findAll();
        $hashItems = [];
        foreach ($items as $it) {
            $hashItems[] = [
                'boi_bo_idx'     => (int) $it->boi_bo_idx,
                'boi_bag_code'   => (string) $it->boi_bag_code,
                'boi_bag_name'   => (string) ($it->boi_bag_name ?? ''),
                'boi_unit_price' => (float) $it->boi_unit_price,
                'boi_qty_box'    => (int) $it->boi_qty_box,
                'boi_qty_sheet'  => (int) $it->boi_qty_sheet,
                'boi_amount'     => (float) $it->boi_amount,
            ];
        }

        $newOrder = $order;
        $newOrder->bo_status = $newStatus;

        $newHash = $this->computeOrderHash($boIdx, $newOrder, $hashItems);
        $actorIdx = session()->get('mb_idx') ? (int) session()->get('mb_idx') : null;
        $lgIdx = (int) ($order->bo_lg_idx ?? 0);

        $seedFilePath = '';
        $ledgerPayload = [
            'bo_idx' => $boIdx,
            'bo_uuid' => (string) $order->bo_uuid,
            'bo_version' => (int) $order->bo_version,
            'bo_lot_no' => (string) $order->bo_lot_no,
            'bo_hash' => $newHash,
            'seed_file' => $seedFilePath,
            'hash_chain' => [$previousHash, $newHash],
            'order' => [
                'bo_status' => $newStatus,
                'bo_hash' => $newHash,
            ],
            'items' => $hashItems,
        ];

        $ledger = new SqlLedger();
        $ledger->appendBlock(
            $txType,
            $ledgerPayload,
            (string) $order->bo_uuid,
            (int) $order->bo_version,
            $actorIdx,
            $lgIdx
        );

        // order row에 hash 반영
        $this->orderModel->update($boIdx, [
            'bo_status' => $newStatus,
            'bo_moddate' => date('Y-m-d H:i:s'),
            'bo_hash' => $newHash,
        ]);
    }

    /**
     * store()에서 생성하는 bo_hash와 동일한 "헤더+items" 규격을 사용해 SHA-256을 계산한다.
     *
     * @param int $boIdx
     * @param object $order
     * @param array<int,array<string,mixed>> $hashItems
     */
    private function computeOrderHash(int $boIdx, object $order, array $hashItems): string
    {
        $orderData = [
            'bo_uuid' => (string) $order->bo_uuid,
            'bo_version' => (int) $order->bo_version,
            'bo_lg_idx' => (int) $order->bo_lg_idx,
            'bo_gugun_code' => (string) ($order->bo_gugun_code ?? ''),
            'bo_dong_code' => (string) ($order->bo_dong_code ?? ''),
            'bo_company_idx' => $order->bo_company_idx !== null ? (int) $order->bo_company_idx : null,
            'bo_agency_idx' => $order->bo_agency_idx !== null ? (int) $order->bo_agency_idx : null,
            'bo_fee_rate' => (float) ($order->bo_fee_rate ?? 0),
            'bo_order_date' => (string) $order->bo_order_date,
            'bo_bag_types' => (string) ($order->bo_bag_types ?? ''),
            'bo_unit_prices' => (string) ($order->bo_unit_prices ?? ''),
            'bo_qty_boxes' => (string) ($order->bo_qty_boxes ?? ''),
            'bo_lot_no' => (string) $order->bo_lot_no,
            'bo_hash' => '',
            'bo_status' => (string) $order->bo_status,
            'bo_orderer_idx' => $order->bo_orderer_idx !== null ? (int) $order->bo_orderer_idx : null,
            'bo_regdate' => (string) ($order->bo_regdate ?? ''),
        ];

        $hashPayload = $orderData;
        $hashPayload['bo_idx'] = $boIdx;
        $hashPayload['items'] = $hashItems;

        $hashJson = json_encode($hashPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        return hash('sha256', $hashJson !== false ? $hashJson : (string) $boIdx);
    }
}
