<?php

declare(strict_types=1);

namespace App\Libraries;

/**
 * 기간별 봉투 수불 현황 집계 (bag/flow)
 */
class BagFlowReportBuilder
{
    private \CodeIgniter\Database\BaseConnection $db;

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

    private static function bagCodeKey(mixed $code): string
    {
        return (string) $code;
    }

    /**
     * @return array{
     *   rows: list<array<string, mixed>>,
     *   bagKindLabels: array<string, string>,
     *   queried: bool
     * }
     */
    public function build(
        int $lgIdx,
        string $startDate,
        string $endDate,
        string $aggMode,
        string $bagCodeFilter,
        string $bagKindFilter,
        int $saIdx,
        bool $queried
    ): array {
        $bagKindLabels = $this->loadBagKindLabels();
        $products = $this->loadProducts($lgIdx, $bagCodeFilter, $bagKindFilter);
        if ($products === [] || ! $queried) {
            return ['rows' => [], 'bagKindLabels' => $bagKindLabels, 'queried' => $queried];
        }

        $codes = array_keys($products);
        $dayBefore = date('Y-m-d', strtotime($startDate . ' -1 day'));
        $openingRaw = $this->aggregateMovements($lgIdx, $codes, $saIdx, null, $dayBefore);
        $opening = $this->collapseOpeningBalances($openingRaw);
        $periodMoves = $this->aggregateMovements($lgIdx, $codes, $saIdx, $startDate, $endDate);

        if ($aggMode === 'daily') {
            $rows = $this->buildDailyRows($products, $opening, $periodMoves, $startDate, $endDate, $bagKindLabels);
        } else {
            $rows = $this->buildPeriodRows($products, $opening, $periodMoves, $startDate, $endDate, $bagKindLabels);
        }

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

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

        return $products;
    }

    /**
     * @return array<string, string>
     */
    private function loadBagKindLabels(): array
    {
        $kindE = model(\App\Models\CodeKindModel::class)->where('ck_code', 'E')->first();
        if (! $kindE) {
            return [];
        }
        $labels = [];
        foreach (model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindE->ck_idx, true, null) as $d) {
            $labels[(string) $d->cd_code] = (string) $d->cd_name;
        }

        return $labels;
    }

    /**
     * @param list<string> $codes
     * @return array<string, array<string, array<string, int>>> bag_code => date => metrics
     */
    private function aggregateMovements(
        int $lgIdx,
        array $codes,
        int $saIdx,
        ?string $fromDate,
        ?string $toDate
    ): array {
        if ($codes === []) {
            return [];
        }

        $buckets = [];
        $ensure = static function (string $code, string $date) use (&$buckets): array {
            if (! isset($buckets[$code][$date])) {
                $buckets[$code][$date] = self::emptyMetrics();
            }

            return $buckets[$code][$date];
        };

        $hasMisc = $this->db->query("SHOW TABLES LIKE 'bag_misc_flow'")->getNumRows() > 0;
        $hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
        $codePlaceholders = implode(',', array_fill(0, count($codes), '?'));

        // 입고(발주 입고)
        $sql = "
            SELECT br_bag_code AS bag_code, br_receive_date AS mv_date, SUM(br_qty_sheet) AS qty
            FROM bag_receiving
            WHERE br_lg_idx = ? AND br_bag_code IN ({$codePlaceholders})
        ";
        $params = array_merge([$lgIdx], $codes);
        if ($fromDate !== null) {
            $sql .= ' AND br_receive_date >= ?';
            $params[] = $fromDate;
        }
        if ($toDate !== null) {
            $sql .= ' AND br_receive_date <= ?';
            $params[] = $toDate;
        }
        $sql .= ' GROUP BY br_bag_code, br_receive_date';
        foreach ($this->db->query($sql, $params)->getResult() as $row) {
            $code = (string) $row->bag_code;
            $date = (string) $row->mv_date;
            $m = $ensure($code, $date);
            $m['recv_in'] += (int) $row->qty;
            $buckets[$code][$date] = $m;
        }

        // 판매·반품(반품=입고)
        $sql = "
            SELECT bs.bs_bag_code AS bag_code, bs.bs_sale_date AS mv_date, bs.bs_type AS mv_type,
                   SUM(ABS(bs.bs_qty)) AS qty
            FROM bag_sale bs
        ";
        if ($saIdx > 0 && $hasDsSa) {
            $sql .= ' INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx AND ds.ds_sa_idx = ?';
        }
        $sql .= " WHERE bs.bs_lg_idx = ? AND bs.bs_bag_code IN ({$codePlaceholders})";
        $params = $saIdx > 0 && $hasDsSa ? [$saIdx, $lgIdx] : [$lgIdx];
        $params = array_merge($params, $codes);
        if ($fromDate !== null) {
            $sql .= ' AND bs.bs_sale_date >= ?';
            $params[] = $fromDate;
        }
        if ($toDate !== null) {
            $sql .= ' AND bs.bs_sale_date <= ?';
            $params[] = $toDate;
        }
        $sql .= ' GROUP BY bs.bs_bag_code, bs.bs_sale_date, bs.bs_type';
        foreach ($this->db->query($sql, $params)->getResult() as $row) {
            $code = (string) $row->bag_code;
            $date = (string) $row->mv_date;
            $qty = (int) $row->qty;
            $m = $ensure($code, $date);
            $type = (string) $row->mv_type;
            if ($type === 'return') {
                $m['recv_return'] += $qty;
            } else {
                $m['out_sale'] += $qty;
            }
            $buckets[$code][$date] = $m;
        }

        // 불출
        $sql = "
            SELECT bi2_bag_code AS bag_code, bi2_issue_date AS mv_date, bi2_issue_type AS issue_type,
                   SUM(bi2_qty) AS qty
            FROM bag_issue
            WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_bag_code IN ({$codePlaceholders})
        ";
        $params = array_merge([$lgIdx], $codes);
        if ($fromDate !== null) {
            $sql .= ' AND bi2_issue_date >= ?';
            $params[] = $fromDate;
        }
        if ($toDate !== null) {
            $sql .= ' AND bi2_issue_date <= ?';
            $params[] = $toDate;
        }
        $sql .= ' GROUP BY bi2_bag_code, bi2_issue_date, bi2_issue_type';
        foreach ($this->db->query($sql, $params)->getResult() as $row) {
            $code = (string) $row->bag_code;
            $date = (string) $row->mv_date;
            $qty = (int) $row->qty;
            $m = $ensure($code, $date);
            $issueType = (string) $row->issue_type;
            if (str_contains($issueType, '무료')) {
                $m['out_issue_free'] += $qty;
            } else {
                $m['out_issue_gen'] += $qty;
            }
            $buckets[$code][$date] = $m;
        }

        if ($hasMisc) {
            $sql = "
                SELECT bmf_bag_code AS bag_code, bmf_date AS mv_date, bmf_type AS mv_type,
                       SUM(bmf_qty) AS qty
                FROM bag_misc_flow
                WHERE bmf_lg_idx = ? AND bmf_bag_code IN ({$codePlaceholders})
            ";
            $params = array_merge([$lgIdx], $codes);
            if ($fromDate !== null) {
                $sql .= ' AND bmf_date >= ?';
                $params[] = $fromDate;
            }
            if ($toDate !== null) {
                $sql .= ' AND bmf_date <= ?';
                $params[] = $toDate;
            }
            $sql .= ' GROUP BY bmf_bag_code, bmf_date, bmf_type';
            foreach ($this->db->query($sql, $params)->getResult() as $row) {
                $code = (string) $row->bag_code;
                $date = (string) $row->mv_date;
                $qty = (int) $row->qty;
                $m = $ensure($code, $date);
                if ((string) $row->mv_type === 'in') {
                    $m['recv_misc'] += $qty;
                } else {
                    $m['out_misc'] += $qty;
                }
                $buckets[$code][$date] = $m;
            }
        }

        $normalized = [];
        foreach ($buckets as $code => $byDate) {
            $key = self::bagCodeKey($code);
            foreach ($byDate as $date => $m) {
                $normalized[$key][$date] = self::finalizeMetrics($m);
            }
        }

        return $normalized;
    }

    /**
     * @param array<string, string> $products
     * @param array<string, array<string, int>> $opening date key '_open'
     * @param array<string, array<string, array<string, int>>> $periodMoves
     * @param array<string, string> $bagKindLabels
     * @return list<array<string, mixed>>
     */
    private function buildPeriodRows(
        array $products,
        array $opening,
        array $periodMoves,
        string $startDate,
        string $endDate,
        array $bagKindLabels
    ): array {
        $periodKey = $startDate . '~' . $endDate;
        $grouped = [];
        foreach ($products as $codeKey => $name) {
            $code = self::bagCodeKey($codeKey);
            $kind = strlen($code) >= 2 ? substr($code, 0, 2) : '';
            $grouped[$kind][] = ['code' => $code, 'name' => $name];
        }
        ksort($grouped);

        $rows = [];
        $grand = self::emptyMetrics();
        $grand['row_type'] = 'grand';
        $grand['date'] = '';
        $grand['item_name'] = '총계';

        foreach ($grouped as $kind => $items) {
            $sub = self::emptyMetrics();
            $sub['row_type'] = 'subtotal';
            $sub['date'] = '';
            $sub['item_name'] = ($bagKindLabels[$kind] ?? '기타') . ' 소계';

            foreach ($items as $item) {
                $code = self::bagCodeKey($item['code']);
                $m = self::emptyMetrics();
                foreach ($periodMoves[$code] ?? [] as $dayMetrics) {
                    $m = self::mergeMetrics($m, $dayMetrics);
                }
                $m = self::finalizeMetrics($m);
                $m['prev_stock'] = (int) ($opening[$code] ?? 0);
                $m['balance'] = $m['prev_stock'] + $m['recv_total'] - $m['out_total'];
                $m['row_type'] = 'data';
                $m['date'] = $periodKey;
                $m['item_name'] = $item['name'];
                $m['bag_code'] = $code;
                $m['bag_kind'] = $kind;
                $rows[] = $m;
                $sub = self::mergeMetrics($sub, $m);
            }

            $sub = self::finalizeMetrics($sub);
            $sub['balance'] = $sub['prev_stock'] + $sub['recv_total'] - $sub['out_total'];
            $rows[] = $sub;
            $grand = self::mergeMetrics($grand, $sub);
        }

        $grand = self::finalizeMetrics($grand);
        $grand['balance'] = $grand['prev_stock'] + $grand['recv_total'] - $grand['out_total'];
        $rows[] = $grand;

        return $rows;
    }

    /**
     * @param array<string, array<string, array<string, int>>> $openingRaw
     * @return array<string, int> bag_code => 전일(기간 전) 재고
     */
    private function collapseOpeningBalances(array $openingRaw): array
    {
        $out = [];
        foreach ($openingRaw as $code => $byDate) {
            $net = self::emptyMetrics();
            foreach ($byDate as $m) {
                $net = self::mergeMetrics($net, $m);
            }
            $net = self::finalizeMetrics($net);
            $out[self::bagCodeKey($code)] = $net['recv_total'] - $net['out_total'];
        }

        return $out;
    }

    /**
     * @param array<string, string> $products
     * @param array<string, array<string, array<string, int>>> $opening
     * @param array<string, array<string, array<string, int>>> $periodMoves
     * @param array<string, string> $bagKindLabels
     * @return list<array<string, mixed>>
     */
    private function buildDailyRows(
        array $products,
        array $opening,
        array $periodMoves,
        string $startDate,
        string $endDate,
        array $bagKindLabels
    ): array {
        $dates = [];
        $cursor = strtotime($startDate);
        $endTs = strtotime($endDate);
        while ($cursor <= $endTs) {
            $dates[] = date('Y-m-d', $cursor);
            $cursor = strtotime('+1 day', $cursor);
        }

        $rows = [];
        foreach ($products as $codeKey => $name) {
            $code = self::bagCodeKey($codeKey);
            $kind = strlen($code) >= 2 ? substr($code, 0, 2) : '';
            $running = (int) ($opening[$code] ?? 0);
            foreach ($dates as $date) {
                $dayM = $periodMoves[$code][$date] ?? self::emptyMetrics();
                $dayM = self::finalizeMetrics($dayM);
                $prev = $running;
                $running = $prev + $dayM['recv_total'] - $dayM['out_total'];
                $dayM['prev_stock'] = $prev;
                $dayM['balance'] = $running;
                $dayM['row_type'] = 'data';
                $dayM['date'] = $date;
                $dayM['item_name'] = $name;
                $dayM['bag_code'] = $code;
                $dayM['bag_kind'] = $kind;
                if ($this->rowHasActivity($dayM)) {
                    $rows[] = $dayM;
                }
            }
        }

        return $rows;
    }

    /**
     * @param array<string, int|float> $m
     */
    private function rowHasActivity(array $m): bool
    {
        foreach (['recv_in', 'recv_return', 'recv_misc', 'out_sale', 'out_issue_gen', 'out_issue_free', 'out_return', 'out_misc'] as $k) {
            if ((int) ($m[$k] ?? 0) !== 0) {
                return true;
            }
        }

        return (int) ($m['prev_stock'] ?? 0) !== 0;
    }

    /**
     * @return array<string, int>
     */
    private static function emptyMetrics(): array
    {
        return [
            'prev_stock'     => 0,
            'recv_in'        => 0,
            'recv_return'    => 0,
            'recv_misc'      => 0,
            'recv_total'     => 0,
            'out_sale'       => 0,
            'out_issue_gen'  => 0,
            'out_issue_free' => 0,
            'out_return'     => 0,
            'out_misc'       => 0,
            'out_total'      => 0,
            'balance'        => 0,
        ];
    }

    /**
     * @param array<string, int> $m
     * @return array<string, int>
     */
    private static function finalizeMetrics(array $m): array
    {
        $m['recv_total'] = (int) $m['recv_in'] + (int) $m['recv_return'] + (int) $m['recv_misc'];
        $m['out_total'] = (int) $m['out_sale'] + (int) $m['out_issue_gen'] + (int) $m['out_issue_free']
            + (int) $m['out_return'] + (int) $m['out_misc'];

        return $m;
    }

    /**
     * @param array<string, int> $a
     * @param array<string, int> $b
     * @return array<string, int>
     */
    private static function mergeMetrics(array $a, array $b): array
    {
        foreach (self::emptyMetrics() as $k => $_) {
            $a[$k] = (int) ($a[$k] ?? 0) + (int) ($b[$k] ?? 0);
        }

        return $a;
    }
}
