<?php

declare(strict_types=1);

namespace App\Libraries;

/**
 * 쓰레기봉투 수급 계획 (레거시 w_gm820r 유추)
 *
 * - 바코드 봉투: bag_receiving_pack_code 에 등록된 품목
 * - 기존 봉투: 그 외 (수기·비바코드 재고)
 * - 소진일수 ≈ (총재고 / 월판매량) × 30
 * - 발주예정일 = 기준일 + 소진일수 − 적정재고보유일수(제작기일)
 * - 긴급(발주예정일 ≤ 기준일): 발주수량 ≈ max(0, 월판매량×18 − 총재고)
 */
class BagSupplyPlanBuilder
{
    private const AVG_SALES_MONTHS = 12;

    /** 긴급 발주 시 목표 보유 개월 수 (레거시 화면 값 유추) */
    private const URGENT_REPLENISH_MONTHS = 18;

    private \CodeIgniter\Database\BaseConnection $db;

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

    /**
     * @return array{
     *   rows: list<array<string, mixed>>,
     *   barcodeCodes: array<string, bool>,
     *   queried: bool
     * }
     */
    public function build(
        int $lgIdx,
        string $refDate,
        int $leadDays,
        string $stockScope,
        string $salesScope,
        bool $queried
    ): array {
        $barcodeCodes = $this->loadBarcodeCodes($lgIdx);
        $products = $this->loadProducts($lgIdx);
        if ($products === [] || ! $queried) {
            return ['rows' => [], 'barcodeCodes' => $barcodeCodes, 'queried' => $queried];
        }

        $inventory = $this->loadInventoryMap($lgIdx);
        $pendingIn = $this->loadPendingInbound($lgIdx);
        $lastOrders = $this->loadLastOrders($lgIdx);
        $monthlySales = $this->loadMonthlyAverageSales($lgIdx, $refDate, $salesScope, $barcodeCodes);
        $movementsSince = $this->loadMovementsSinceOrders($lgIdx, $lastOrders, $refDate);

        $rows = [];
        foreach ($products as $code => $name) {
            $isBarcode = isset($barcodeCodes[$code]);
            $rawStock = (int) ($inventory[$code] ?? 0);
            $currentStock = $this->scopedStock($rawStock, $stockScope, $isBarcode);
            $pendingQty = $this->scopedPending((int) ($pendingIn[$code] ?? 0), $stockScope, $isBarcode);
            $totalStock = $currentStock + $pendingQty;
            $monthlyFloat = (float) ($monthlySales[$code] ?? 0.0);
            $monthlyAvg   = (int) round($monthlyFloat);

            $depletionDays = $this->calcDepletionDays($totalStock, $monthlyFloat);
            $scheduleDate  = $this->calcScheduleDate($refDate, $depletionDays, $leadDays);
            $orderQty      = $this->calcOrderQty($refDate, $scheduleDate, $depletionDays, $leadDays, $monthlyAvg, $totalStock);

            $last = $lastOrders[$code] ?? null;
            $orderDate = $last ? (string) ($last['order_date'] ?? '') : '';
            $lastQty = $last ? (int) ($last['qty_sheet'] ?? 0) : 0;
            $stockAtOrder = 0;
            if ($orderDate !== '' && $lastQty > 0) {
                $mv = $movementsSince[$code] ?? ['sale' => 0, 'recv' => 0, 'issue' => 0];
                $stockAtOrder = max(0, $rawStock + $mv['sale'] - $mv['recv'] - $mv['issue']);
            }

            $rows[] = [
                'bag_code'          => $code,
                'bag_name'          => $name,
                'is_barcode'        => $isBarcode,
                'last_order_date'   => $orderDate,
                'last_order_qty'    => $lastQty,
                'stock_at_order'    => $stockAtOrder,
                'current_stock'     => $currentStock,
                'pending_inbound'   => $pendingQty,
                'total_stock'       => $totalStock,
                'monthly_avg_sales' => $monthlyAvg,
                'depletion_days'    => $depletionDays,
                'schedule_date'     => $scheduleDate,
                'schedule_overdue'  => $scheduleDate !== '' && $scheduleDate <= $refDate && $depletionDays < 99999,
                'order_qty'         => $orderQty,
            ];
        }

        usort($rows, static fn (array $a, array $b): int => strcmp((string) $a['bag_code'], (string) $b['bag_code']));

        return ['rows' => $rows, 'barcodeCodes' => $barcodeCodes, 'queried' => true];
    }

    /**
     * @return array<string, bool>
     */
    private function loadBarcodeCodes(int $lgIdx): array
    {
        if (! $this->db->tableExists('bag_receiving_pack_code')) {
            return [];
        }
        $set = [];
        foreach ($this->db->table('bag_receiving_pack_code')
            ->select('brpc_bag_code')
            ->distinct()
            ->where('brpc_lg_idx', $lgIdx)
            ->where('brpc_bag_code !=', '')
            ->get()
            ->getResultArray() as $row) {
            $code = trim((string) ($row['brpc_bag_code'] ?? ''));
            if ($code !== '') {
                $set[$code] = true;
            }
        }

        return $set;
    }

    /**
     * @return array<string, string> code => name
     */
    private function loadProducts(int $lgIdx): array
    {
        $kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
        $products = [];
        if ($kindO) {
            foreach (model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) as $d) {
                $code = trim((string) ($d->cd_code ?? ''));
                if ($code !== '') {
                    $products[$code] = trim((string) ($d->cd_name ?? $code));
                }
            }
        }

        foreach ($this->db->table('bag_inventory')
            ->select('bi_bag_code, bi_bag_name')
            ->where('bi_lg_idx', $lgIdx)
            ->get()
            ->getResultArray() as $row) {
            $code = trim((string) ($row['bi_bag_code'] ?? ''));
            if ($code === '') {
                continue;
            }
            if (! isset($products[$code])) {
                $products[$code] = trim((string) ($row['bi_bag_name'] ?? $code));
            }
        }

        return $products;
    }

    /**
     * @return array<string, int>
     */
    private function loadInventoryMap(int $lgIdx): array
    {
        $map = [];
        foreach (model(\App\Models\BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll() as $inv) {
            $code = trim((string) ($inv->bi_bag_code ?? ''));
            if ($code !== '') {
                $map[$code] = (int) ($inv->bi_qty ?? 0);
            }
        }

        return $map;
    }

    /**
     * @return array<string, int>
     */
    private function loadPendingInbound(int $lgIdx): array
    {
        $map = [];
        $sql = "
            SELECT boi.boi_bag_code AS bag_code,
                   SUM(GREATEST(0, CAST(boi.boi_qty_sheet AS SIGNED) - IFNULL(r.recv_qty, 0))) AS pending_qty
            FROM bag_order_item boi
            INNER JOIN bag_order bo ON bo.bo_idx = boi.boi_bo_idx
            LEFT JOIN (
                SELECT br_bo_idx, br_bag_code, SUM(br_qty_sheet) AS recv_qty
                FROM bag_receiving
                GROUP BY br_bo_idx, br_bag_code
            ) r ON r.br_bo_idx = bo.bo_idx AND r.br_bag_code = boi.boi_bag_code
            WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
            GROUP BY boi.boi_bag_code
        ";
        foreach ($this->db->query($sql, [$lgIdx])->getResult() as $row) {
            $qty = (int) ($row->pending_qty ?? 0);
            if ($qty > 0) {
                $map[(string) $row->bag_code] = $qty;
            }
        }

        return $map;
    }

    /**
     * @return array<string, array{order_date: string, qty_sheet: int, bag_name: string}>
     */
    private function loadLastOrders(int $lgIdx): array
    {
        $map = [];
        $supportsWindow = $this->db->DBDriver === 'MySQLi';
        if ($supportsWindow) {
            $sql = "
                SELECT bag_code, order_date, qty_sheet, bag_name
                FROM (
                    SELECT boi.boi_bag_code AS bag_code, bo.bo_order_date AS order_date,
                           boi.boi_qty_sheet AS qty_sheet, boi.boi_bag_name AS bag_name,
                           ROW_NUMBER() OVER (
                               PARTITION BY boi.boi_bag_code
                               ORDER BY bo.bo_order_date DESC, bo.bo_idx DESC
                           ) AS rn
                    FROM bag_order_item boi
                    INNER JOIN bag_order bo ON bo.bo_idx = boi.boi_bo_idx
                    WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
                ) t
                WHERE t.rn = 1
            ";
            foreach ($this->db->query($sql, [$lgIdx])->getResultArray() as $row) {
                $code = trim((string) ($row['bag_code'] ?? ''));
                if ($code === '') {
                    continue;
                }
                $map[$code] = [
                    'order_date' => (string) ($row['order_date'] ?? ''),
                    'qty_sheet'  => (int) ($row['qty_sheet'] ?? 0),
                    'bag_name'   => (string) ($row['bag_name'] ?? ''),
                ];
            }

            return $map;
        }

        $sql = "
            SELECT boi.boi_bag_code AS bag_code, MAX(bo.bo_order_date) AS order_date
            FROM bag_order_item boi
            INNER JOIN bag_order bo ON bo.bo_idx = boi.boi_bo_idx
            WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
            GROUP BY boi.boi_bag_code
        ";
        foreach ($this->db->query($sql, [$lgIdx])->getResultArray() as $row) {
            $code = trim((string) ($row['bag_code'] ?? ''));
            $orderDate = (string) ($row['order_date'] ?? '');
            if ($code === '' || $orderDate === '') {
                continue;
            }
            $item = $this->db->query("
                SELECT boi.boi_qty_sheet AS qty_sheet, boi.boi_bag_name AS bag_name
                FROM bag_order_item boi
                INNER JOIN bag_order bo ON bo.bo_idx = boi.boi_bo_idx
                WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
                  AND boi.boi_bag_code = ? AND bo.bo_order_date = ?
                ORDER BY bo.bo_idx DESC
                LIMIT 1
            ", [$lgIdx, $code, $orderDate])->getRowArray();
            if ($item) {
                $map[$code] = [
                    'order_date' => $orderDate,
                    'qty_sheet'  => (int) ($item['qty_sheet'] ?? 0),
                    'bag_name'   => (string) ($item['bag_name'] ?? ''),
                ];
            }
        }

        return $map;
    }

    /**
     * @param array<string, array{order_date: string, qty_sheet: int, bag_name: string}> $lastOrders
     * @return array<string, array{sale: int, recv: int, issue: int}>
     */
    private function loadMovementsSinceOrders(int $lgIdx, array $lastOrders, string $refDate): array
    {
        $byCode = [];
        $minDate = $refDate;
        foreach ($lastOrders as $code => $info) {
            $d = (string) ($info['order_date'] ?? '');
            if ($d === '') {
                continue;
            }
            $byCode[$code] = $d;
            if ($d < $minDate) {
                $minDate = $d;
            }
        }
        if ($byCode === []) {
            return [];
        }

        $codes = array_keys($byCode);
        $placeholders = implode(',', array_fill(0, count($codes), '?'));
        $params = array_merge([$lgIdx], $codes, [$minDate, $refDate]);
        $out = [];
        foreach ($codes as $code) {
            $out[$code] = ['sale' => 0, 'recv' => 0, 'issue' => 0];
        }

        $sql = "
            SELECT bs_bag_code AS bag_code, bs_sale_date AS mv_date,
                   SUM(CASE WHEN bs_type IN ('sale','cancel') THEN ABS(bs_qty) ELSE 0 END) AS sale_qty
            FROM bag_sale
            WHERE bs_lg_idx = ? AND bs_bag_code IN ({$placeholders})
              AND bs_sale_date >= ? AND bs_sale_date <= ?
            GROUP BY bs_bag_code, bs_sale_date
        ";
        foreach ($this->db->query($sql, $params)->getResult() as $row) {
            $code = (string) $row->bag_code;
            $orderDate = $byCode[$code] ?? '';
            if ($orderDate === '' || (string) $row->mv_date < $orderDate) {
                continue;
            }
            $out[$code]['sale'] += (int) $row->sale_qty;
        }

        $sql = "
            SELECT br_bag_code AS bag_code, br_receive_date AS mv_date, SUM(br_qty_sheet) AS recv_qty
            FROM bag_receiving
            WHERE br_lg_idx = ? AND br_bag_code IN ({$placeholders})
              AND br_receive_date >= ? AND br_receive_date <= ?
            GROUP BY br_bag_code, br_receive_date
        ";
        foreach ($this->db->query($sql, $params)->getResult() as $row) {
            $code = (string) $row->bag_code;
            $orderDate = $byCode[$code] ?? '';
            if ($orderDate === '' || (string) $row->mv_date < $orderDate) {
                continue;
            }
            $out[$code]['recv'] += (int) $row->recv_qty;
        }

        $sql = "
            SELECT bi2_bag_code AS bag_code, bi2_issue_date AS mv_date, SUM(bi2_qty) AS issue_qty
            FROM bag_issue
            WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_bag_code IN ({$placeholders})
              AND bi2_issue_date >= ? AND bi2_issue_date <= ?
            GROUP BY bi2_bag_code, bi2_issue_date
        ";
        foreach ($this->db->query($sql, $params)->getResult() as $row) {
            $code = (string) $row->bag_code;
            $orderDate = $byCode[$code] ?? '';
            if ($orderDate === '' || (string) $row->mv_date < $orderDate) {
                continue;
            }
            $out[$code]['issue'] += (int) $row->issue_qty;
        }

        return $out;
    }

    /**
     * @param array<string, bool> $barcodeCodes
     * @return array<string, float>
     */
    private function loadMonthlyAverageSales(
        int $lgIdx,
        string $refDate,
        string $salesScope,
        array $barcodeCodes
    ): array {
        $fromDate = date('Y-m-d', strtotime($refDate . ' -' . self::AVG_SALES_MONTHS . ' months'));
        $legacyNet = [];
        $barcodeNet = [];

        foreach ($this->db->query("
            SELECT bs_bag_code AS bag_code,
                   SUM(CASE WHEN bs_type = 'sale' THEN ABS(bs_qty)
                            WHEN bs_type IN ('return','cancel') THEN -ABS(bs_qty)
                            ELSE 0 END) AS net_qty
            FROM bag_sale
            WHERE bs_lg_idx = ? AND bs_sale_date > ? AND bs_sale_date <= ?
            GROUP BY bs_bag_code
        ", [$lgIdx, $fromDate, $refDate])->getResult() as $row) {
            $code = (string) $row->bag_code;
            $legacyNet[$code] = (float) ($row->net_qty ?? 0) / self::AVG_SALES_MONTHS;
        }

        if ($this->db->tableExists('bag_sale_scan_code')) {
            foreach ($this->db->query("
                SELECT bssc_bag_code AS bag_code, SUM(bssc_qty) AS net_qty
                FROM bag_sale_scan_code
                WHERE bssc_lg_idx = ? AND bssc_state = 'sold'
                  AND DATE(bssc_regdate) > ? AND DATE(bssc_regdate) <= ?
                GROUP BY bssc_bag_code
            ", [$lgIdx, $fromDate, $refDate])->getResult() as $row) {
                $code = (string) $row->bag_code;
                $barcodeNet[$code] = (float) ($row->net_qty ?? 0) / self::AVG_SALES_MONTHS;
            }
        }

        $merged = [];
        $allCodes = array_unique(array_merge(array_keys($legacyNet), array_keys($barcodeNet)));
        foreach ($allCodes as $code) {
            $isBarcode = isset($barcodeCodes[$code]);
            $legacy = $legacyNet[$code] ?? 0.0;
            $scan = $barcodeNet[$code] ?? 0.0;
            $merged[$code] = match ($salesScope) {
                'legacy'  => $isBarcode ? 0.0 : $legacy,
                'barcode' => $isBarcode ? ($scan > 0 ? $scan : $legacy) : 0.0,
                default   => $isBarcode && $scan > 0 ? $scan : $legacy,
            };
        }

        return $merged;
    }

    private function scopedStock(int $qty, string $scope, bool $isBarcode): int
    {
        return match ($scope) {
            'legacy'  => $isBarcode ? 0 : $qty,
            'barcode' => $isBarcode ? $qty : 0,
            default   => $qty,
        };
    }

    private function scopedPending(int $qty, string $scope, bool $isBarcode): int
    {
        return $this->scopedStock($qty, $scope, $isBarcode);
    }

    private function calcDepletionDays(int $totalStock, float $monthlyAvg): int
    {
        if ($monthlyAvg <= 0.0) {
            return 0;
        }

        return (int) round(($totalStock / $monthlyAvg) * 30);
    }

    private function calcScheduleDate(string $refDate, int $depletionDays, int $leadDays): string
    {
        if ($depletionDays <= 0) {
            return '';
        }
        if ($depletionDays >= 99999) {
            return '';
        }

        $offset = $depletionDays - $leadDays;
        if ($offset < 0) {
            return $refDate;
        }

        $base = \DateTimeImmutable::createFromFormat('Y-m-d', $refDate);
        if ($base === false) {
            return '';
        }

        try {
            $scheduled = $base->modify('+' . $offset . ' days');
        } catch (\Exception) {
            return '';
        }

        $year = (int) $scheduled->format('Y');
        $refYear = (int) $base->format('Y');
        if ($year < $refYear - 1 || $year > $refYear + 120) {
            return '';
        }

        return $scheduled->format('Y-m-d');
    }

    private function calcOrderQty(
        string $refDate,
        string $scheduleDate,
        int $depletionDays,
        int $leadDays,
        int $monthlyAvg,
        int $totalStock
    ): int {
        if ($monthlyAvg <= 0) {
            return 0;
        }
        $urgent = $scheduleDate !== '' && $scheduleDate <= $refDate;
        $lowStock = $depletionDays > 0 && $depletionDays <= $leadDays && $scheduleDate !== '';
        if (! $urgent && ! $lowStock) {
            return 0;
        }

        $target = (int) round($monthlyAvg * ($urgent ? self::URGENT_REPLENISH_MONTHS : max(2, (int) ceil($leadDays / 30.0))));

        return max(0, $target - $totalStock);
    }
}
